Updated to readonly public props on as many models as possible

This commit is contained in:
Alejandro Celaya 2022-04-23 14:00:47 +02:00
parent e79391907a
commit bca3e62ced
74 changed files with 249 additions and 494 deletions

View file

@ -53,7 +53,7 @@ class DomainRedirectsCommand extends Command
/** @var string[] $availableDomains */
$availableDomains = invoke(
filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault()),
filter($this->domainService->listDomains(), static fn (DomainItem $item) => ! $item->isDefault),
'toString',
);
if (empty($availableDomains)) {

View file

@ -48,12 +48,12 @@ class ListDomainsCommand extends Command
$table->render(
$showRedirects ? [...$commonFields, '"Not found" redirects'] : $commonFields,
map($domains, function (DomainItem $domain) use ($showRedirects) {
$commonValues = [$domain->toString(), $domain->isDefault() ? 'Yes' : 'No'];
$commonValues = [$domain->toString(), $domain->isDefault ? 'Yes' : 'No'];
return $showRedirects
? [
...$commonValues,
$this->notFoundRedirectsToString($domain->notFoundRedirectConfig()),
$this->notFoundRedirectsToString($domain->notFoundRedirectConfig),
]
: $commonValues;
}),

View file

@ -81,6 +81,6 @@ class DeleteShortUrlCommand extends Command
private function runDelete(SymfonyStyle $io, ShortUrlIdentifier $identifier, bool $ignoreThreshold): void
{
$this->deleteShortUrlService->deleteByShortCode($identifier, $ignoreThreshold);
$io->success(sprintf('Short URL with short code "%s" successfully deleted.', $identifier->shortCode()));
$io->success(sprintf('Short URL with short code "%s" successfully deleted.', $identifier->shortCode));
}
}

View file

@ -46,7 +46,7 @@ class ListTagsCommand extends Command
return map(
$tags,
static fn (TagInfo $tagInfo) => [$tagInfo->tag(), $tagInfo->shortUrlsCount(), $tagInfo->visitsCount()],
static fn (TagInfo $tagInfo) => [$tagInfo->tag, $tagInfo->shortUrlsCount, $tagInfo->visitsCount],
);
}
}

View file

@ -22,11 +22,11 @@ abstract class AbstractLockedCommand extends Command
final protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$lockConfig = $this->getLockConfig();
$lock = $this->locker->createLock($lockConfig->lockName(), $lockConfig->ttl(), $lockConfig->isBlocking());
$lock = $this->locker->createLock($lockConfig->lockName, $lockConfig->ttl, $lockConfig->isBlocking);
if (! $lock->acquire($lockConfig->isBlocking())) {
if (! $lock->acquire($lockConfig->isBlocking)) {
$output->writeln(
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName()),
sprintf('<comment>Command "%s" is already in progress. Skipping.</comment>', $lockConfig->lockName),
);
return ExitCodes::EXIT_WARNING;
}

View file

@ -9,9 +9,9 @@ final class LockedCommandConfig
public const DEFAULT_TTL = 600.0; // 10 minutes
private function __construct(
private string $lockName,
private bool $isBlocking,
private float $ttl = self::DEFAULT_TTL,
public readonly string $lockName,
public readonly bool $isBlocking,
public readonly float $ttl = self::DEFAULT_TTL,
) {
}
@ -24,19 +24,4 @@ final class LockedCommandConfig
{
return new self($lockName, false);
}
public function lockName(): string
{
return $this->lockName;
}
public function isBlocking(): bool
{
return $this->isBlocking;
}
public function ttl(): float
{
return $this->ttl;
}
}

View file

@ -15,7 +15,7 @@ final class ShlinkTable
private const DEFAULT_STYLE_NAME = 'default';
private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
private function __construct(private Table $baseTable, private bool $withRowSeparators)
private function __construct(private readonly Table $baseTable, private readonly bool $withRowSeparators)
{
}

View file

@ -36,10 +36,11 @@ class DeleteShortUrlCommandTest extends TestCase
public function successMessageIsPrintedIfUrlIsProperlyDeleted(): void
{
$shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode(new ShortUrlIdentifier($shortCode), false)->will(
function (): void {
},
);
$deleteByShortCode = $this->service->deleteByShortCode(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
false,
)->will(function (): void {
});
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
@ -55,7 +56,7 @@ class DeleteShortUrlCommandTest extends TestCase
public function invalidShortCodePrintsMessage(): void
{
$shortCode = 'abc123';
$identifier = new ShortUrlIdentifier($shortCode);
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
$deleteByShortCode = $this->service->deleteByShortCode($identifier, false)->willThrow(
Exception\ShortUrlNotFoundException::fromNotFound($identifier),
);
@ -77,7 +78,7 @@ class DeleteShortUrlCommandTest extends TestCase
string $expectedMessage,
): void {
$shortCode = 'abc123';
$identifier = new ShortUrlIdentifier($shortCode);
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode);
$deleteByShortCode = $this->service->deleteByShortCode($identifier, Argument::type('bool'))->will(
function (array $args) use ($shortCode): void {
$ignoreThreshold = array_pop($args);
@ -114,12 +115,13 @@ class DeleteShortUrlCommandTest extends TestCase
public function deleteIsNotRetriedWhenThresholdIsReachedAndQuestionIsDeclined(): void
{
$shortCode = 'abc123';
$deleteByShortCode = $this->service->deleteByShortCode(new ShortUrlIdentifier($shortCode), false)->willThrow(
Exception\DeleteShortUrlException::fromVisitsThreshold(
10,
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
),
);
$deleteByShortCode = $this->service->deleteByShortCode(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
false,
)->willThrow(Exception\DeleteShortUrlException::fromVisitsThreshold(
10,
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
));
$this->commandTester->setInputs(['no']);
$this->commandTester->execute(['shortCode' => $shortCode]);

View file

@ -44,7 +44,7 @@ class GetVisitsCommandTest extends TestCase
{
$shortCode = 'abc123';
$this->visitsHelper->visitsForShortUrl(
new ShortUrlIdentifier($shortCode),
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(DateRange::emptyInstance()),
)
->willReturn(new Paginator(new ArrayAdapter([])))
@ -60,7 +60,7 @@ class GetVisitsCommandTest extends TestCase
$startDate = '2016-01-01';
$endDate = '2016-02-01';
$this->visitsHelper->visitsForShortUrl(
new ShortUrlIdentifier($shortCode),
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(DateRange::withStartAndEndDate(Chronos::parse($startDate), Chronos::parse($endDate))),
)
->willReturn(new Paginator(new ArrayAdapter([])))
@ -79,7 +79,7 @@ class GetVisitsCommandTest extends TestCase
$shortCode = 'abc123';
$startDate = 'foo';
$info = $this->visitsHelper->visitsForShortUrl(
new ShortUrlIdentifier($shortCode),
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
new VisitsParams(DateRange::emptyInstance()),
)->willReturn(new Paginator(new ArrayAdapter([])));
@ -100,7 +100,10 @@ class GetVisitsCommandTest extends TestCase
public function outputIsProperlyGenerated(): void
{
$shortCode = 'abc123';
$this->visitsHelper->visitsForShortUrl(new ShortUrlIdentifier($shortCode), Argument::any())->willReturn(
$this->visitsHelper->visitsForShortUrl(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
Argument::any(),
)->willReturn(
new Paginator(new ArrayAdapter([
Visit::forValidShortUrl(ShortUrl::createEmpty(), new Visitor('bar', 'foo', '', ''))->locate(
VisitLocation::fromGeolocation(new Location('', 'Spain', '', '', 0, 0, '')),

View file

@ -37,8 +37,9 @@ class ResolveUrlCommandTest extends TestCase
$shortCode = 'abc123';
$expectedUrl = 'http://domain.com/foo/bar';
$shortUrl = ShortUrl::withLongUrl($expectedUrl);
$this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode))->willReturn($shortUrl)
->shouldBeCalledOnce();
$this->urlResolver->resolveShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode))->willReturn(
$shortUrl,
)->shouldBeCalledOnce();
$this->commandTester->execute(['shortCode' => $shortCode]);
$output = $this->commandTester->getDisplay();
@ -48,8 +49,8 @@ class ResolveUrlCommandTest extends TestCase
/** @test */
public function incorrectShortCodeOutputsErrorMessage(): void
{
$identifier = new ShortUrlIdentifier('abc123');
$shortCode = $identifier->shortCode();
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain('abc123');
$shortCode = $identifier->shortCode;
$this->urlResolver->resolveShortUrl($identifier)
->willThrow(ShortUrlNotFoundException::fromNotFound($identifier))

View file

@ -29,11 +29,11 @@ final class QrCodeParams
private const SUPPORTED_FORMATS = ['png', 'svg'];
private function __construct(
private int $size,
private int $margin,
private WriterInterface $writer,
private ErrorCorrectionLevelInterface $errorCorrectionLevel,
private RoundBlockSizeModeInterface $roundBlockSizeMode,
public readonly int $size,
public readonly int $margin,
public readonly WriterInterface $writer,
public readonly ErrorCorrectionLevelInterface $errorCorrectionLevel,
public readonly RoundBlockSizeModeInterface $roundBlockSizeMode,
) {
}
@ -105,29 +105,4 @@ final class QrCodeParams
{
return strtolower(trim($param));
}
public function size(): int
{
return $this->size;
}
public function margin(): int
{
return $this->margin;
}
public function writer(): WriterInterface
{
return $this->writer;
}
public function errorCorrectionLevel(): ErrorCorrectionLevelInterface
{
return $this->errorCorrectionLevel;
}
public function roundBlockSizeMode(): RoundBlockSizeModeInterface
{
return $this->roundBlockSizeMode;
}
}

View file

@ -42,11 +42,11 @@ class QrCodeAction implements MiddlewareInterface
$params = QrCodeParams::fromRequest($request, $this->defaultOptions);
$qrCodeBuilder = Builder::create()
->data($this->stringifier->stringify($shortUrl))
->size($params->size())
->margin($params->margin())
->writer($params->writer())
->errorCorrectionLevel($params->errorCorrectionLevel())
->roundBlockSizeMode($params->roundBlockSizeMode());
->size($params->size)
->margin($params->margin)
->writer($params->writer)
->errorCorrectionLevel($params->errorCorrectionLevel)
->roundBlockSizeMode($params->roundBlockSizeMode);
return new QrCodeResponse($qrCodeBuilder->build());
}

View file

@ -9,9 +9,9 @@ use JsonSerializable;
final class NotFoundRedirects implements JsonSerializable
{
private function __construct(
private ?string $baseUrlRedirect,
private ?string $regular404Redirect,
private ?string $invalidShortUrlRedirect,
public readonly ?string $baseUrlRedirect,
public readonly ?string $regular404Redirect,
public readonly ?string $invalidShortUrlRedirect,
) {
}
@ -33,21 +33,6 @@ final class NotFoundRedirects implements JsonSerializable
return new self($config->baseUrlRedirect(), $config->regular404Redirect(), $config->invalidShortUrlRedirect());
}
public function baseUrlRedirect(): ?string
{
return $this->baseUrlRedirect;
}
public function regular404Redirect(): ?string
{
return $this->regular404Redirect;
}
public function invalidShortUrlRedirect(): ?string
{
return $this->invalidShortUrlRedirect;
}
public function jsonSerialize(): array
{
return [

View file

@ -12,9 +12,9 @@ use Shlinkio\Shlink\Core\Entity\Domain;
final class DomainItem implements JsonSerializable
{
private function __construct(
private string $authority,
private NotFoundRedirectConfigInterface $notFoundRedirectConfig,
private bool $isDefault,
private readonly string $authority,
public readonly NotFoundRedirectConfigInterface $notFoundRedirectConfig,
public readonly bool $isDefault,
) {
}
@ -23,9 +23,9 @@ final class DomainItem implements JsonSerializable
return new self($domain->getAuthority(), $domain, false);
}
public static function forDefaultDomain(string $authority, NotFoundRedirectConfigInterface $config): self
public static function forDefaultDomain(string $defaultDomain, NotFoundRedirectConfigInterface $config): self
{
return new self($authority, $config, true);
return new self($defaultDomain, $config, true);
}
public function jsonSerialize(): array
@ -41,14 +41,4 @@ final class DomainItem implements JsonSerializable
{
return $this->authority;
}
public function isDefault(): bool
{
return $this->isDefault;
}
public function notFoundRedirectConfig(): NotFoundRedirectConfigInterface
{
return $this->notFoundRedirectConfig;
}
}

View file

@ -66,8 +66,8 @@ class Domain extends AbstractEntity implements JsonSerializable, NotFoundRedirec
public function configureNotFoundRedirects(NotFoundRedirects $redirects): void
{
$this->baseUrlRedirect = $redirects->baseUrlRedirect();
$this->regular404Redirect = $redirects->regular404Redirect();
$this->invalidShortUrlRedirect = $redirects->invalidShortUrlRedirect();
$this->baseUrlRedirect = $redirects->baseUrlRedirect;
$this->regular404Redirect = $redirects->regular404Redirect;
$this->invalidShortUrlRedirect = $redirects->invalidShortUrlRedirect;
}
}

View file

@ -89,10 +89,10 @@ class Visit extends AbstractEntity implements JsonSerializable
private function hydrateFromVisitor(Visitor $visitor, bool $anonymize = true): void
{
$this->userAgent = $visitor->getUserAgent();
$this->referer = $visitor->getReferer();
$this->remoteAddr = $this->processAddress($anonymize, $visitor->getRemoteAddress());
$this->visitedUrl = $visitor->getVisitedUrl();
$this->userAgent = $visitor->userAgent;
$this->referer = $visitor->referer;
$this->remoteAddr = $this->processAddress($anonymize, $visitor->remoteAddress);
$this->visitedUrl = $visitor->visitedUrl;
$this->potentialBot = $visitor->isPotentialBot();
}

View file

@ -8,15 +8,10 @@ use JsonSerializable;
abstract class AbstractVisitEvent implements JsonSerializable
{
public function __construct(protected string $visitId)
public function __construct(public readonly string $visitId)
{
}
public function visitId(): string
{
return $this->visitId;
}
public function jsonSerialize(): array
{
return ['visitId' => $this->visitId];

View file

@ -6,13 +6,8 @@ namespace Shlinkio\Shlink\Core\EventDispatcher\Event;
final class UrlVisited extends AbstractVisitEvent
{
public function __construct(string $visitId, private ?string $originalIpAddress = null)
public function __construct(string $visitId, public readonly ?string $originalIpAddress = null)
{
parent::__construct($visitId);
}
public function originalIpAddress(): ?string
{
return $this->originalIpAddress;
}
}

View file

@ -30,7 +30,7 @@ class LocateVisit
public function __invoke(UrlVisited $shortUrlVisited): void
{
$visitId = $shortUrlVisited->visitId();
$visitId = $shortUrlVisited->visitId;
/** @var Visit|null $visit */
$visit = $this->em->find(Visit::class, $visitId);
@ -41,7 +41,7 @@ class LocateVisit
return;
}
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress(), $visit);
$this->locateVisit($visitId, $shortUrlVisited->originalIpAddress, $visit);
$this->eventDispatcher->dispatch(new VisitLocated($visitId));
}

View file

@ -27,7 +27,7 @@ class NotifyVisitToMercure
public function __invoke(VisitLocated $shortUrlLocated): void
{
$visitId = $shortUrlLocated->visitId();
$visitId = $shortUrlLocated->visitId;
/** @var Visit|null $visit */
$visit = $this->em->find(Visit::class, $visitId);

View file

@ -37,7 +37,7 @@ class NotifyVisitToRabbitMq
return;
}
$visitId = $shortUrlLocated->visitId();
$visitId = $shortUrlLocated->visitId;
$visit = $this->em->find(Visit::class, $visitId);
if ($visit === null) {

View file

@ -40,7 +40,7 @@ class NotifyVisitToWebHooks
return;
}
$visitId = $shortUrlLocated->visitId();
$visitId = $shortUrlLocated->visitId;
/** @var Visit|null $visit */
$visit = $this->em->find(Visit::class, $visitId);

View file

@ -20,8 +20,8 @@ class DeleteShortUrlException extends DomainException implements ProblemDetailsE
public static function fromVisitsThreshold(int $threshold, ShortUrlIdentifier $identifier): self
{
$shortCode = $identifier->shortCode();
$domain = $identifier->domain();
$shortCode = $identifier->shortCode;
$domain = $identifier->domain;
$suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain);
$e = new self(sprintf(
'Impossible to delete short URL with short code "%s"%s, since it has more than "%s" visits.',

View file

@ -20,8 +20,8 @@ class ShortUrlNotFoundException extends DomainException implements ProblemDetail
public static function fromNotFound(ShortUrlIdentifier $identifier): self
{
$shortCode = $identifier->shortCode();
$domain = $identifier->domain();
$shortCode = $identifier->shortCode;
$domain = $identifier->domain;
$suffix = $domain === null ? '' : sprintf(' for domain "%s"', $domain);
$e = new self(sprintf('No URL found with short code "%s"%s', $shortCode, $suffix));

View file

@ -14,7 +14,7 @@ use function sprintf;
final class ShortUrlImporting
{
private function __construct(private ShortUrl $shortUrl, private bool $isNew)
private function __construct(private readonly ShortUrl $shortUrl, private readonly bool $isNew)
{
}

View file

@ -10,8 +10,8 @@ abstract class AbstractInfinitePaginableListParams
{
private const FIRST_PAGE = 1;
private int $page;
private int $itemsPerPage;
public readonly int $page;
public readonly int $itemsPerPage;
protected function __construct(?int $page, ?int $itemsPerPage)
{
@ -28,14 +28,4 @@ abstract class AbstractInfinitePaginableListParams
{
return $itemsPerPage === null || $itemsPerPage < 0 ? Paginator::ALL_ITEMS : $itemsPerPage;
}
public function getPage(): int
{
return $this->page;
}
public function getItemsPerPage(): int
{
return $this->itemsPerPage;
}
}

View file

@ -8,7 +8,7 @@ final class Ordering
{
private const DEFAULT_DIR = 'ASC';
private function __construct(private ?string $field, private string $dir)
private function __construct(public readonly ?string $field, public readonly string $direction)
{
}
@ -26,16 +26,6 @@ final class Ordering
return self::fromTuple([null, null]);
}
public function orderField(): ?string
{
return $this->field;
}
public function orderDirection(): string
{
return $this->dir;
}
public function hasOrderField(): bool
{
return $this->field !== null;

View file

@ -10,7 +10,7 @@ use Symfony\Component\Console\Input\InputInterface;
final class ShortUrlIdentifier
{
public function __construct(private string $shortCode, private ?string $domain = null)
public function __construct(public readonly string $shortCode, public readonly ?string $domain = null)
{
}
@ -54,14 +54,4 @@ final class ShortUrlIdentifier
{
return new self($shortCode, $domain);
}
public function shortCode(): string
{
return $this->shortCode;
}
public function domain(): ?string
{
return $this->domain;
}
}

View file

@ -18,10 +18,10 @@ final class Visitor
public const REMOTE_ADDRESS_MAX_LENGTH = 256;
public const VISITED_URL_MAX_LENGTH = 2048;
private string $userAgent;
private string $referer;
private string $visitedUrl;
private ?string $remoteAddress;
public readonly string $userAgent;
public readonly string $referer;
public readonly string $visitedUrl;
public readonly ?string $remoteAddress;
private bool $potentialBot;
public function __construct(string $userAgent, string $referer, ?string $remoteAddress, string $visitedUrl)
@ -61,26 +61,6 @@ final class Visitor
return new self('cf-facebook', '', null, '');
}
public function getUserAgent(): string
{
return $this->userAgent;
}
public function getReferer(): string
{
return $this->referer;
}
public function getRemoteAddress(): ?string
{
return $this->remoteAddress;
}
public function getVisitedUrl(): string
{
return $this->visitedUrl;
}
public function isPotentialBot(): bool
{
return $this->potentialBot;

View file

@ -10,13 +10,13 @@ use function Shlinkio\Shlink\Core\parseDateRangeFromQuery;
final class VisitsParams extends AbstractInfinitePaginableListParams
{
private DateRange $dateRange;
public readonly DateRange $dateRange;
public function __construct(
?DateRange $dateRange = null,
?int $page = null,
?int $itemsPerPage = null,
private bool $excludeBots = false,
public readonly bool $excludeBots = false,
) {
parent::__construct($page, $itemsPerPage);
$this->dateRange = $dateRange ?? DateRange::emptyInstance();
@ -31,14 +31,4 @@ final class VisitsParams extends AbstractInfinitePaginableListParams
isset($query['excludeBots']),
);
}
public function getDateRange(): DateRange
{
return $this->dateRange;
}
public function excludeBots(): bool
{
return $this->excludeBots;
}
}

View file

@ -47,8 +47,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
private function processOrderByForList(QueryBuilder $qb, Ordering $orderBy): array
{
$fieldName = $orderBy->orderField();
$order = $orderBy->orderDirection();
$fieldName = $orderBy->field;
$order = $orderBy->direction;
if ($fieldName === 'visits') {
// FIXME This query is inefficient.
@ -146,8 +146,8 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
$query = $this->getEntityManager()->createQuery($dql);
$query->setMaxResults(1)
->setParameters([
'shortCode' => $identifier->shortCode(),
'domain' => $identifier->domain(),
'shortCode' => $identifier->shortCode,
'domain' => $identifier->domain,
]);
// Since we ordered by domain, we will have first the URL matching provided domain, followed by the one
@ -198,10 +198,10 @@ class ShortUrlRepository extends EntitySpecificationRepository implements ShortU
$qb->from(ShortUrl::class, 's')
->where($qb->expr()->isNotNull('s.shortCode'))
->andWhere($qb->expr()->eq('s.shortCode', ':slug'))
->setParameter('slug', $identifier->shortCode())
->setParameter('slug', $identifier->shortCode)
->setMaxResults(1);
$this->whereDomainIs($qb, $identifier->domain());
$this->whereDomainIs($qb, $identifier->domain);
$this->applySpecification($qb, $spec, 's');

View file

@ -41,8 +41,8 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
*/
public function findTagsWithInfo(?TagsListFiltering $filtering = null): array
{
$orderField = $filtering?->orderBy()?->orderField();
$orderDir = $filtering?->orderBy()?->orderDirection();
$orderField = $filtering?->orderBy?->field;
$orderDir = $filtering?->orderBy?->direction;
$orderMainQuery = contains(['shortUrlsCount', 'visitsCount'], $orderField);
$conn = $this->getEntityManager()->getConnection();
@ -51,16 +51,16 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
if (! $orderMainQuery) {
$subQb->orderBy('t.name', $orderDir ?? 'ASC')
->setMaxResults($filtering?->limit() ?? PHP_INT_MAX)
->setFirstResult($filtering?->offset() ?? 0);
->setMaxResults($filtering?->limit ?? PHP_INT_MAX)
->setFirstResult($filtering?->offset ?? 0);
}
$searchTerm = $filtering?->searchTerm();
$searchTerm = $filtering?->searchTerm;
if ($searchTerm !== null) {
$subQb->andWhere($subQb->expr()->like('t.name', $conn->quote('%' . $searchTerm . '%')));
}
$apiKey = $filtering?->apiKey();
$apiKey = $filtering?->apiKey;
$this->applySpecification($subQb, new WithInlinedApiKeySpecsEnsuringJoin($apiKey), 't');
// A native query builder needs to be used here, because DQL and ORM query builders do not support
@ -97,8 +97,8 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
$orderField === 'shortUrlsCount' ? 'short_urls_count' : 'visits_count',
$orderDir ?? 'ASC',
)
->setMaxResults($filtering?->limit() ?? PHP_INT_MAX)
->setFirstResult($filtering?->offset() ?? 0);
->setMaxResults($filtering?->limit ?? PHP_INT_MAX)
->setFirstResult($filtering?->offset ?? 0);
}
// Add ordering by tag name, as a fallback in case of same amount, or as default ordering

View file

@ -86,7 +86,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
public function findVisitsByShortCode(ShortUrlIdentifier $identifier, VisitsListFiltering $filtering): array
{
$qb = $this->createVisitsByShortCodeQueryBuilder($identifier, $filtering);
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
}
public function countVisitsByShortCode(ShortUrlIdentifier $identifier, VisitsCountFiltering $filtering): int
@ -103,7 +103,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
): QueryBuilder {
/** @var ShortUrlRepositoryInterface $shortUrlRepo */
$shortUrlRepo = $this->getEntityManager()->getRepository(ShortUrl::class);
$shortUrlId = $shortUrlRepo->findOne($identifier, $filtering->apiKey()?->spec())?->getId() ?? '-1';
$shortUrlId = $shortUrlRepo->findOne($identifier, $filtering->apiKey?->spec())?->getId() ?? '-1';
// Parameters in this query need to be part of the query itself, as we need to use it as sub-query later
// Since they are not provided by the caller, it's reasonably safe
@ -111,12 +111,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
$qb->from(Visit::class, 'v')
->where($qb->expr()->eq('v.shortUrl', $shortUrlId));
if ($filtering->excludeBots()) {
if ($filtering->excludeBots) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
// Apply date range filtering
$this->applyDatesInline($qb, $filtering->dateRange());
$this->applyDatesInline($qb, $filtering->dateRange);
return $qb;
}
@ -124,7 +124,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
public function findVisitsByTag(string $tag, VisitsListFiltering $filtering): array
{
$qb = $this->createVisitsByTagQueryBuilder($tag, $filtering);
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
}
public function countVisitsByTag(string $tag, VisitsCountFiltering $filtering): int
@ -144,12 +144,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
->join('s.tags', 't')
->where($qb->expr()->eq('t.name', $this->getEntityManager()->getConnection()->quote($tag)));
if ($filtering->excludeBots()) {
if ($filtering->excludeBots) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
$this->applyDatesInline($qb, $filtering->dateRange());
$this->applySpecification($qb, $filtering->apiKey()?->inlinedSpec(), 'v');
$this->applyDatesInline($qb, $filtering->dateRange);
$this->applySpecification($qb, $filtering->apiKey?->inlinedSpec(), 'v');
return $qb;
}
@ -160,7 +160,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
public function findVisitsByDomain(string $domain, VisitsListFiltering $filtering): array
{
$qb = $this->createVisitsByDomainQueryBuilder($domain, $filtering);
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
}
public function countVisitsByDomain(string $domain, VisitsCountFiltering $filtering): int
@ -185,12 +185,12 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
->where($qb->expr()->eq('d.authority', $this->getEntityManager()->getConnection()->quote($domain)));
}
if ($filtering->excludeBots()) {
if ($filtering->excludeBots) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
$this->applyDatesInline($qb, $filtering->dateRange());
$this->applySpecification($qb, $filtering->apiKey()?->inlinedSpec(), 'v');
$this->applyDatesInline($qb, $filtering->dateRange);
$this->applySpecification($qb, $filtering->apiKey?->inlinedSpec(), 'v');
return $qb;
}
@ -199,7 +199,7 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
{
$qb = $this->createAllVisitsQueryBuilder($filtering);
$qb->andWhere($qb->expr()->isNull('v.shortUrl'));
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
}
public function countOrphanVisits(VisitsCountFiltering $filtering): int
@ -215,9 +215,9 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
$qb = $this->createAllVisitsQueryBuilder($filtering);
$qb->andWhere($qb->expr()->isNotNull('v.shortUrl'));
$this->applySpecification($qb, $filtering->apiKey()?->inlinedSpec());
$this->applySpecification($qb, $filtering->apiKey?->inlinedSpec());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit(), $filtering->offset());
return $this->resolveVisitsWithNativeQuery($qb, $filtering->limit, $filtering->offset);
}
public function countNonOrphanVisits(VisitsCountFiltering $filtering): int
@ -232,11 +232,11 @@ class VisitRepository extends EntitySpecificationRepository implements VisitRepo
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->from(Visit::class, 'v');
if ($filtering->excludeBots()) {
if ($filtering->excludeBots) {
$qb->andWhere($qb->expr()->eq('v.potentialBot', 'false'));
}
$this->applyDatesInline($qb, $filtering->dateRange());
$this->applyDatesInline($qb, $filtering->dateRange);
return $qb;
}

View file

@ -8,23 +8,11 @@ use JsonSerializable;
final class TagInfo implements JsonSerializable
{
public function __construct(private string $tag, private int $shortUrlsCount, private int $visitsCount)
{
}
public function tag(): string
{
return $this->tag;
}
public function shortUrlsCount(): int
{
return $this->shortUrlsCount;
}
public function visitsCount(): int
{
return $this->visitsCount;
public function __construct(
public readonly string $tag,
public readonly int $shortUrlsCount,
public readonly int $visitsCount,
) {
}
public function jsonSerialize(): array

View file

@ -10,7 +10,7 @@ use function sprintf;
final class TagRenaming
{
private function __construct(private string $oldName, private string $newName)
private function __construct(public readonly string $oldName, public readonly string $newName)
{
}
@ -31,16 +31,6 @@ final class TagRenaming
return self::fromNames($payload['oldName'], $payload['newName']);
}
public function oldName(): string
{
return $this->oldName;
}
public function newName(): string
{
return $this->newName;
}
public function nameChanged(): bool
{
return $this->oldName !== $this->newName;

View file

@ -10,41 +10,16 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
final class TagsListFiltering
{
public function __construct(
private ?int $limit = null,
private ?int $offset = null,
private ?string $searchTerm = null,
private ?Ordering $orderBy = null,
private ?ApiKey $apiKey = null,
public readonly ?int $limit = null,
public readonly ?int $offset = null,
public readonly ?string $searchTerm = null,
public readonly ?Ordering $orderBy = null,
public readonly ?ApiKey $apiKey = null,
) {
}
public static function fromRangeAndParams(int $limit, int $offset, TagsParams $params, ?ApiKey $apiKey): self
{
return new self($limit, $offset, $params->searchTerm(), $params->orderBy(), $apiKey);
}
public function limit(): ?int
{
return $this->limit;
}
public function offset(): ?int
{
return $this->offset;
}
public function searchTerm(): ?string
{
return $this->searchTerm;
}
public function orderBy(): ?Ordering
{
return $this->orderBy;
}
public function apiKey(): ?ApiKey
{
return $this->apiKey;
return new self($limit, $offset, $params->searchTerm, $params->orderBy, $apiKey);
}
}

View file

@ -12,9 +12,9 @@ use function Shlinkio\Shlink\Common\parseOrderBy;
final class TagsParams extends AbstractInfinitePaginableListParams
{
private function __construct(
private ?string $searchTerm,
private Ordering $orderBy,
private bool $withStats,
public readonly ?string $searchTerm,
public readonly Ordering $orderBy,
public readonly bool $withStats,
?int $page,
?int $itemsPerPage,
) {
@ -31,19 +31,4 @@ final class TagsParams extends AbstractInfinitePaginableListParams
isset($query['itemsPerPage']) ? (int) $query['itemsPerPage'] : null,
);
}
public function searchTerm(): ?string
{
return $this->searchTerm;
}
public function orderBy(): Ordering
{
return $this->orderBy;
}
public function withStats(): bool
{
return $this->withStats;
}
}

View file

@ -30,7 +30,7 @@ abstract class AbstractTagsPaginatorAdapter implements AdapterInterface
new WithApiKeySpecsEnsuringJoin($this->apiKey),
];
$searchTerm = $this->params->searchTerm();
$searchTerm = $this->params->searchTerm;
if ($searchTerm !== null) {
$conditions[] = Spec::like('name', $searchTerm);
}

View file

@ -15,13 +15,13 @@ class TagsPaginatorAdapter extends AbstractTagsPaginatorAdapter
new WithApiKeySpecsEnsuringJoin($this->apiKey),
Spec::orderBy(
'name', // Ordering by other fields makes no sense here
$this->params->orderBy()->orderDirection(),
$this->params->orderBy->direction,
),
Spec::limit($length),
Spec::offset($offset),
];
$searchTerm = $this->params->searchTerm();
$searchTerm = $this->params->searchTerm;
if ($searchTerm !== null) {
$conditions[] = Spec::like('name', $searchTerm);
}

View file

@ -49,8 +49,8 @@ class TagService implements TagServiceInterface
private function createPaginator(AdapterInterface $adapter, TagsParams $params): Paginator
{
return (new Paginator($adapter))
->setMaxPerPage($params->getItemsPerPage())
->setCurrentPage($params->getPage());
->setMaxPerPage($params->itemsPerPage)
->setCurrentPage($params->page);
}
/**
@ -83,17 +83,17 @@ class TagService implements TagServiceInterface
$repo = $this->em->getRepository(Tag::class);
/** @var Tag|null $tag */
$tag = $repo->findOneBy(['name' => $renaming->oldName()]);
$tag = $repo->findOneBy(['name' => $renaming->oldName]);
if ($tag === null) {
throw TagNotFoundException::fromTag($renaming->oldName());
throw TagNotFoundException::fromTag($renaming->oldName);
}
$newNameExists = $renaming->nameChanged() && $repo->count(['name' => $renaming->newName()]) > 0;
$newNameExists = $renaming->nameChanged() && $repo->count(['name' => $renaming->newName]) > 0;
if ($newNameExists) {
throw TagConflictException::forExistingTag($renaming);
}
$tag->rename($renaming->newName());
$tag->rename($renaming->newName);
$this->em->flush();
return $tag;

View file

@ -26,8 +26,8 @@ class DomainVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
return $this->visitRepository->countVisitsByDomain(
$this->domain,
new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
),
);
@ -38,8 +38,8 @@ class DomainVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
return $this->visitRepository->findVisitsByDomain(
$this->domain,
new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$length,
$offset,

View file

@ -23,8 +23,8 @@ class NonOrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAda
protected function doCount(): int
{
return $this->repo->countNonOrphanVisits(new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
));
}
@ -32,8 +32,8 @@ class NonOrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAda
public function getSlice(int $offset, int $length): iterable
{
return $this->repo->findNonOrphanVisits(new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$length,
$offset,

View file

@ -19,16 +19,16 @@ class OrphanVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapte
protected function doCount(): int
{
return $this->repo->countOrphanVisits(new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
));
}
public function getSlice(int $offset, int $length): iterable
{
return $this->repo->findOrphanVisits(new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
null,
$length,
$offset,

View file

@ -27,8 +27,8 @@ class ShortUrlVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdap
return $this->visitRepository->findVisitsByShortCode(
$this->identifier,
new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$length,
$offset,
@ -41,8 +41,8 @@ class ShortUrlVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdap
return $this->visitRepository->countVisitsByShortCode(
$this->identifier,
new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
),
);

View file

@ -26,8 +26,8 @@ class TagVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
return $this->visitRepository->findVisitsByTag(
$this->tag,
new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$length,
$offset,
@ -40,8 +40,8 @@ class TagVisitsPaginatorAdapter extends AbstractCacheableCountPaginatorAdapter
return $this->visitRepository->countVisitsByTag(
$this->tag,
new VisitsCountFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
),
);

View file

@ -10,9 +10,9 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
class VisitsCountFiltering
{
public function __construct(
private ?DateRange $dateRange = null,
private bool $excludeBots = false,
private ?ApiKey $apiKey = null,
public readonly ?DateRange $dateRange = null,
public readonly bool $excludeBots = false,
public readonly ?ApiKey $apiKey = null,
) {
}
@ -20,19 +20,4 @@ class VisitsCountFiltering
{
return new self(null, false, $apiKey);
}
public function dateRange(): ?DateRange
{
return $this->dateRange;
}
public function excludeBots(): bool
{
return $this->excludeBots;
}
public function apiKey(): ?ApiKey
{
return $this->apiKey;
}
}

View file

@ -13,19 +13,9 @@ final class VisitsListFiltering extends VisitsCountFiltering
?DateRange $dateRange = null,
bool $excludeBots = false,
?ApiKey $apiKey = null,
private ?int $limit = null,
private ?int $offset = null,
public readonly ?int $limit = null,
public readonly ?int $offset = null,
) {
parent::__construct($dateRange, $excludeBots, $apiKey);
}
public function limit(): ?int
{
return $this->limit;
}
public function offset(): ?int
{
return $this->offset;
}
}

View file

@ -22,14 +22,14 @@ class CountOfNonOrphanVisits extends BaseSpecification
{
$conditions = [
Spec::isNotNull('shortUrl'),
new InDateRange($this->filtering->dateRange()),
new InDateRange($this->filtering->dateRange),
];
if ($this->filtering->excludeBots()) {
if ($this->filtering->excludeBots) {
$conditions[] = Spec::eq('potentialBot', false);
}
$apiKey = $this->filtering->apiKey();
$apiKey = $this->filtering->apiKey;
if ($apiKey !== null) {
$conditions[] = new WithApiKeySpecsEnsuringJoin($apiKey, 'shortUrl');
}

View file

@ -21,10 +21,10 @@ class CountOfOrphanVisits extends BaseSpecification
{
$conditions = [
Spec::isNull('shortUrl'),
new InDateRange($this->filtering->dateRange()),
new InDateRange($this->filtering->dateRange),
];
if ($this->filtering->excludeBots()) {
if ($this->filtering->excludeBots) {
$conditions[] = Spec::eq('potentialBot', false);
}

View file

@ -129,8 +129,8 @@ class VisitsStatsHelper implements VisitsStatsHelperInterface
private function createPaginator(AdapterInterface $adapter, VisitsParams $params): Paginator
{
$paginator = new Paginator($adapter);
$paginator->setMaxPerPage($params->getItemsPerPage())
->setCurrentPage($params->getPage());
$paginator->setMaxPerPage($params->itemsPerPage)
->setCurrentPage($params->page);
return $paginator;
}

View file

@ -72,6 +72,6 @@ class VisitsTracker implements VisitsTrackerInterface
$this->em->persist($visit);
$this->em->flush();
$this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->getRemoteAddress()));
$this->eventDispatcher->dispatch(new UrlVisited($visit->getId(), $visitor->remoteAddress));
}
}

View file

@ -64,7 +64,7 @@ class TagRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->persist(new Tag($name));
}
$apiKey = $filtering?->apiKey();
$apiKey = $filtering?->apiKey;
if ($apiKey !== null) {
$this->getEntityManager()->persist($apiKey);
}
@ -101,9 +101,9 @@ class TagRepositoryTest extends DatabaseTestCase
self::assertCount(count($expectedList), $result);
foreach ($expectedList as $index => [$tag, $shortUrlsCount, $visitsCount]) {
self::assertEquals($shortUrlsCount, $result[$index]->shortUrlsCount());
self::assertEquals($visitsCount, $result[$index]->visitsCount());
self::assertEquals($tag, $result[$index]->tag());
self::assertEquals($shortUrlsCount, $result[$index]->shortUrlsCount);
self::assertEquals($visitsCount, $result[$index]->visitsCount);
self::assertEquals($tag, $result[$index]->tag);
}
}

View file

@ -37,9 +37,10 @@ class PixelActionTest extends TestCase
public function imageIsReturned(): void
{
$shortCode = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))->willReturn(
ShortUrl::withLongUrl('http://domain.com/foo/bar'),
)->shouldBeCalledOnce();
$this->urlResolver->resolveEnabledShortUrl(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''),
)->willReturn(ShortUrl::withLongUrl('http://domain.com/foo/bar'))
->shouldBeCalledOnce();
$this->requestTracker->trackIfApplicable(Argument::cetera())->shouldBeCalledOnce();
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode);

View file

@ -59,7 +59,7 @@ class QrCodeActionTest extends TestCase
public function aNotFoundShortCodeWillDelegateIntoNextMiddleware(): void
{
$shortCode = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''))
->willThrow(ShortUrlNotFoundException::class)
->shouldBeCalledOnce();
$delegate = $this->prophesize(RequestHandlerInterface::class);
@ -74,7 +74,7 @@ class QrCodeActionTest extends TestCase
public function aCorrectRequestReturnsTheQrCodeResponse(): void
{
$shortCode = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''))
->willReturn(ShortUrl::createEmpty())
->shouldBeCalledOnce();
$delegate = $this->prophesize(RequestHandlerInterface::class);
@ -100,7 +100,7 @@ class QrCodeActionTest extends TestCase
): void {
$this->options->setFromArray(['format' => $defaultFormat]);
$code = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($code, ''))->willReturn(
ShortUrl::createEmpty(),
);
$delegate = $this->prophesize(RequestHandlerInterface::class);
@ -134,7 +134,7 @@ class QrCodeActionTest extends TestCase
): void {
$this->options->setFromArray($defaults);
$code = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($code, ''))->willReturn(
ShortUrl::createEmpty(),
);
$delegate = $this->prophesize(RequestHandlerInterface::class);
@ -214,7 +214,7 @@ class QrCodeActionTest extends TestCase
->withQueryParams(['size' => 250, 'roundBlockSize' => $roundBlockSize])
->withAttribute('shortCode', $code);
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($code, ''))->willReturn(
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($code, ''))->willReturn(
ShortUrl::withLongUrl('https://shlink.io'),
);
$delegate = $this->prophesize(RequestHandlerInterface::class);

View file

@ -54,7 +54,7 @@ class RedirectActionTest extends TestCase
$shortCode = 'abc123';
$shortUrl = ShortUrl::withLongUrl(self::LONG_URL);
$shortCodeToUrl = $this->urlResolver->resolveEnabledShortUrl(
new ShortUrlIdentifier($shortCode, ''),
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''),
)->willReturn($shortUrl);
$track = $this->requestTracker->trackIfApplicable(Argument::cetera())->will(function (): void {
});
@ -74,7 +74,7 @@ class RedirectActionTest extends TestCase
public function nextMiddlewareIsInvokedIfLongUrlIsNotFound(): void
{
$shortCode = 'abc123';
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode, ''))
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, ''))
->willThrow(ShortUrlNotFoundException::class)
->shouldBeCalledOnce();
$this->requestTracker->trackIfApplicable(Argument::cetera())->shouldNotBeCalled();

View file

@ -24,7 +24,7 @@ class ShortUrlNotFoundExceptionTest extends TestCase
$expectedAdditional['domain'] = $domain;
}
$e = ShortUrlNotFoundException::fromNotFound(new ShortUrlIdentifier($shortCode, $domain));
$e = ShortUrlNotFoundException::fromNotFound(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, $domain));
self::assertEquals($expectedMessage, $e->getMessage());
self::assertEquals($expectedMessage, $e->getDetail());

View file

@ -24,9 +24,9 @@ class VisitorTest extends TestCase
$visitor = new Visitor(...$params);
['userAgent' => $userAgent, 'referer' => $referer, 'remoteAddress' => $remoteAddress] = $expected;
self::assertEquals($userAgent, $visitor->getUserAgent());
self::assertEquals($referer, $visitor->getReferer());
self::assertEquals($remoteAddress, $visitor->getRemoteAddress());
self::assertEquals($userAgent, $visitor->userAgent);
self::assertEquals($referer, $visitor->referer);
self::assertEquals($remoteAddress, $visitor->remoteAddress);
}
public function provideParams(): iterable
@ -89,11 +89,11 @@ class VisitorTest extends TestCase
]));
self::assertNotSame($visitor, $normalizedVisitor);
self::assertEmpty($normalizedVisitor->getUserAgent());
self::assertNotEmpty($visitor->getUserAgent());
self::assertEmpty($normalizedVisitor->getReferer());
self::assertNotEmpty($visitor->getReferer());
self::assertNull($normalizedVisitor->getRemoteAddress());
self::assertNotNull($visitor->getRemoteAddress());
self::assertEmpty($normalizedVisitor->userAgent);
self::assertNotEmpty($visitor->userAgent);
self::assertEmpty($normalizedVisitor->referer);
self::assertNotEmpty($visitor->referer);
self::assertNull($normalizedVisitor->remoteAddress);
self::assertNotNull($visitor->remoteAddress);
}
}

View file

@ -55,7 +55,7 @@ class DeleteShortUrlServiceTest extends TestCase
$this->shortCode,
));
$service->deleteByShortCode(new ShortUrlIdentifier($this->shortCode));
$service->deleteByShortCode(ShortUrlIdentifier::fromShortCodeAndDomain($this->shortCode));
}
/** @test */
@ -66,7 +66,7 @@ class DeleteShortUrlServiceTest extends TestCase
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
$flush = $this->em->flush()->willReturn(null);
$service->deleteByShortCode(new ShortUrlIdentifier($this->shortCode), true);
$service->deleteByShortCode(ShortUrlIdentifier::fromShortCodeAndDomain($this->shortCode), true);
$remove->shouldHaveBeenCalledOnce();
$flush->shouldHaveBeenCalledOnce();
@ -80,7 +80,7 @@ class DeleteShortUrlServiceTest extends TestCase
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
$flush = $this->em->flush()->willReturn(null);
$service->deleteByShortCode(new ShortUrlIdentifier($this->shortCode));
$service->deleteByShortCode(ShortUrlIdentifier::fromShortCodeAndDomain($this->shortCode));
$remove->shouldHaveBeenCalledOnce();
$flush->shouldHaveBeenCalledOnce();
@ -94,7 +94,7 @@ class DeleteShortUrlServiceTest extends TestCase
$remove = $this->em->remove(Argument::type(ShortUrl::class))->willReturn(null);
$flush = $this->em->flush()->willReturn(null);
$service->deleteByShortCode(new ShortUrlIdentifier($this->shortCode));
$service->deleteByShortCode(ShortUrlIdentifier::fromShortCodeAndDomain($this->shortCode));
$remove->shouldHaveBeenCalledOnce();
$flush->shouldHaveBeenCalledOnce();

View file

@ -91,7 +91,7 @@ class ShortUrlResolverTest extends TestCase
)->willReturn($shortUrl);
$getRepo = $this->em->getRepository(ShortUrl::class)->willReturn($repo->reveal());
$result = $this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode));
$result = $this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode));
self::assertSame($shortUrl, $result);
$findOneByShortCode->shouldHaveBeenCalledOnce();
@ -116,7 +116,7 @@ class ShortUrlResolverTest extends TestCase
$findOneByShortCode->shouldBeCalledOnce();
$getRepo->shouldBeCalledOnce();
$this->urlResolver->resolveEnabledShortUrl(new ShortUrlIdentifier($shortCode));
$this->urlResolver->resolveEnabledShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode));
}
public function provideDisabledShortUrls(): iterable

View file

@ -88,7 +88,7 @@ class ShortUrlServiceTest extends TestCase
$shortUrl = ShortUrl::withLongUrl($originalLongUrl);
$findShortUrl = $this->urlResolver->resolveShortUrl(
new ShortUrlIdentifier('abc123'),
ShortUrlIdentifier::fromShortCodeAndDomain('abc123'),
$apiKey,
)->willReturn($shortUrl);
$flush = $this->em->flush()->willReturn(null);
@ -97,7 +97,11 @@ class ShortUrlServiceTest extends TestCase
$shortUrlEdit,
);
$result = $this->service->updateShortUrl(new ShortUrlIdentifier('abc123'), $shortUrlEdit, $apiKey);
$result = $this->service->updateShortUrl(
ShortUrlIdentifier::fromShortCodeAndDomain('abc123'),
$shortUrlEdit,
$apiKey,
);
self::assertSame($shortUrl, $result);
self::assertEquals($shortUrlEdit->validSince(), $shortUrl->getValidSince());

View file

@ -39,7 +39,7 @@ class NonOrphanVisitsPaginatorAdapterTest extends TestCase
{
$expectedCount = 5;
$repoCount = $this->repo->countNonOrphanVisits(
new VisitsCountFiltering($this->params->getDateRange(), $this->params->excludeBots(), $this->apiKey),
new VisitsCountFiltering($this->params->dateRange, $this->params->excludeBots, $this->apiKey),
)->willReturn($expectedCount);
$result = $this->adapter->getNbResults();
@ -57,8 +57,8 @@ class NonOrphanVisitsPaginatorAdapterTest extends TestCase
$visitor = Visitor::emptyInstance();
$list = [Visit::forRegularNotFound($visitor), Visit::forInvalidShortUrl($visitor)];
$repoFind = $this->repo->findNonOrphanVisits(new VisitsListFiltering(
$this->params->getDateRange(),
$this->params->excludeBots(),
$this->params->dateRange,
$this->params->excludeBots,
$this->apiKey,
$limit,
$offset,

View file

@ -35,7 +35,7 @@ class OrphanVisitsPaginatorAdapterTest extends TestCase
{
$expectedCount = 5;
$repoCount = $this->repo->countOrphanVisits(
new VisitsCountFiltering($this->params->getDateRange()),
new VisitsCountFiltering($this->params->dateRange),
)->willReturn($expectedCount);
$result = $this->adapter->getNbResults();
@ -53,7 +53,7 @@ class OrphanVisitsPaginatorAdapterTest extends TestCase
$visitor = Visitor::emptyInstance();
$list = [Visit::forRegularNotFound($visitor), Visit::forInvalidShortUrl($visitor)];
$repoFind = $this->repo->findOrphanVisits(
new VisitsListFiltering($this->params->getDateRange(), $this->params->excludeBots(), null, $limit, $offset),
new VisitsListFiltering($this->params->dateRange, $this->params->excludeBots, null, $limit, $offset),
)->willReturn($list);
$result = $this->adapter->getSlice($offset, $limit);

View file

@ -68,7 +68,7 @@ class ShortUrlVisitsPaginatorAdapterTest extends TestCase
{
return new ShortUrlVisitsPaginatorAdapter(
$this->repo->reveal(),
new ShortUrlIdentifier(''),
ShortUrlIdentifier::fromShortCodeAndDomain(''),
VisitsParams::fromRawData([]),
$apiKey,
);

View file

@ -32,7 +32,7 @@ class ListTagsAction extends AbstractRestAction
$params = TagsParams::fromRawData($request->getQueryParams());
$apiKey = AuthenticationMiddleware::apiKeyFromRequest($request);
if (! $params->withStats()) {
if (! $params->withStats) {
return new JsonResponse([
'tags' => $this->serializePaginator($this->tagService->listTags($params, $apiKey)),
]);
@ -41,7 +41,7 @@ class ListTagsAction extends AbstractRestAction
// This part is deprecated. To get tags with stats, the /tags/stats endpoint should be used instead
$tagsInfo = $this->tagService->tagsInfo($params, $apiKey);
$rawTags = $this->serializePaginator($tagsInfo, null, 'stats');
$rawTags['data'] = map($tagsInfo, static fn (TagInfo $info) => $info->tag());
$rawTags['data'] = map($tagsInfo, static fn (TagInfo $info) => $info->tag);
return new JsonResponse(['tags' => $rawTags]);
}

View file

@ -8,11 +8,13 @@ use Cake\Chronos\Chronos;
final class ApiKeyMeta
{
/**
* @param RoleDefinition[] $roleDefinitions
*/
private function __construct(
private ?string $name,
private ?Chronos $expirationDate,
/** @var RoleDefinition[] */
private array $roleDefinitions,
public readonly ?string $name,
public readonly ?Chronos $expirationDate,
public readonly array $roleDefinitions,
) {
}
@ -35,22 +37,4 @@ final class ApiKeyMeta
{
return new self(null, null, $roleDefinitions);
}
public function name(): ?string
{
return $this->name;
}
public function expirationDate(): ?Chronos
{
return $this->expirationDate;
}
/**
* @return RoleDefinition[]
*/
public function roleDefinitions(): array
{
return $this->roleDefinitions;
}
}

View file

@ -9,7 +9,7 @@ use Shlinkio\Shlink\Rest\ApiKey\Role;
final class RoleDefinition
{
private function __construct(private string $roleName, private array $meta)
private function __construct(public readonly string $roleName, public readonly array $meta)
{
}
@ -25,14 +25,4 @@ final class RoleDefinition
['domain_id' => $domain->getId(), 'authority' => $domain->getAuthority()],
);
}
public function roleName(): string
{
return $this->roleName;
}
public function meta(): array
{
return $this->meta;
}
}

View file

@ -44,8 +44,8 @@ class ApiKey extends AbstractEntity
public static function fromMeta(ApiKeyMeta $meta): self
{
$apiKey = new self($meta->name(), $meta->expirationDate());
foreach ($meta->roleDefinitions() as $roleDefinition) {
$apiKey = new self($meta->name, $meta->expirationDate);
foreach ($meta->roleDefinitions as $roleDefinition) {
$apiKey->registerRole($roleDefinition);
}
@ -137,21 +137,16 @@ class ApiKey extends AbstractEntity
public function registerRole(RoleDefinition $roleDefinition): void
{
$roleName = $roleDefinition->roleName();
$meta = $roleDefinition->meta();
$roleName = $roleDefinition->roleName;
$meta = $roleDefinition->meta;
if ($this->hasRole($roleName)) {
/** @var ApiKeyRole $role */
$role = $this->roles->get($roleName);
$role->updateMeta($meta);
} else {
$role = new ApiKeyRole($roleDefinition->roleName(), $roleDefinition->meta(), $this);
$role = new ApiKeyRole($roleDefinition->roleName, $roleDefinition->meta, $this);
$this->roles[$roleName] = $role;
}
}
public function removeRole(string $roleName): void
{
$this->roles->remove($roleName);
}
}

View file

@ -49,7 +49,7 @@ class AuthenticationMiddleware implements MiddlewareInterface, StatusCodeInterfa
throw VerifyAuthenticationException::forInvalidApiKey();
}
return $handler->handle($request->withAttribute(ApiKey::class, $result->apiKey()));
return $handler->handle($request->withAttribute(ApiKey::class, $result->apiKey));
}
public static function apiKeyFromRequest(Request $request): ApiKey

View file

@ -8,7 +8,7 @@ use Shlinkio\Shlink\Rest\Entity\ApiKey;
final class ApiKeyCheckResult
{
public function __construct(private ?ApiKey $apiKey = null)
public function __construct(public readonly ?ApiKey $apiKey = null)
{
}
@ -16,9 +16,4 @@ final class ApiKeyCheckResult
{
return $this->apiKey !== null && $this->apiKey->isValid();
}
public function apiKey(): ?ApiKey
{
return $this->apiKey;
}
}

View file

@ -44,9 +44,9 @@ class DomainRedirectsRequestTest extends TestCase
$notFound = $request->toNotFoundRedirects($defaults);
self::assertEquals($expectedAuthority, $request->authority());
self::assertEquals($expectedBaseUrlRedirect, $notFound->baseUrlRedirect());
self::assertEquals($expectedRegular404Redirect, $notFound->regular404Redirect());
self::assertEquals($expectedInvalidShortUrlRedirect, $notFound->invalidShortUrlRedirect());
self::assertEquals($expectedBaseUrlRedirect, $notFound->baseUrlRedirect);
self::assertEquals($expectedRegular404Redirect, $notFound->regular404Redirect);
self::assertEquals($expectedInvalidShortUrlRedirect, $notFound->invalidShortUrlRedirect);
}
public function provideValidData(): iterable

View file

@ -36,9 +36,11 @@ class ResolveShortUrlActionTest extends TestCase
{
$shortCode = 'abc123';
$apiKey = ApiKey::create();
$this->urlResolver->resolveShortUrl(new ShortUrlIdentifier($shortCode), $apiKey)->willReturn(
ShortUrl::withLongUrl('http://domain.com/foo/bar'),
)->shouldBeCalledOnce();
$this->urlResolver->resolveShortUrl(
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
$apiKey,
)->willReturn(ShortUrl::withLongUrl('http://domain.com/foo/bar'))
->shouldBeCalledOnce();
$request = (new ServerRequest())->withAttribute('shortCode', $shortCode)->withAttribute(ApiKey::class, $apiKey);
$response = $this->action->handle($request);

View file

@ -38,7 +38,7 @@ class ShortUrlVisitsActionTest extends TestCase
{
$shortCode = 'abc123';
$this->visitsHelper->visitsForShortUrl(
new ShortUrlIdentifier($shortCode),
ShortUrlIdentifier::fromShortCodeAndDomain($shortCode),
Argument::type(VisitsParams::class),
Argument::type(ApiKey::class),
)->willReturn(new Paginator(new ArrayAdapter([])))
@ -52,7 +52,7 @@ class ShortUrlVisitsActionTest extends TestCase
public function paramsAreReadFromQuery(): void
{
$shortCode = 'abc123';
$this->visitsHelper->visitsForShortUrl(new ShortUrlIdentifier($shortCode), new VisitsParams(
$this->visitsHelper->visitsForShortUrl(ShortUrlIdentifier::fromShortCodeAndDomain($shortCode), new VisitsParams(
DateRange::withEndDate(Chronos::parse('2016-01-01 00:00:00')),
3,
10,

View file

@ -16,8 +16,8 @@ class RoleDefinitionTest extends TestCase
{
$definition = RoleDefinition::forAuthoredShortUrls();
self::assertEquals(Role::AUTHORED_SHORT_URLS, $definition->roleName());
self::assertEquals([], $definition->meta());
self::assertEquals(Role::AUTHORED_SHORT_URLS, $definition->roleName);
self::assertEquals([], $definition->meta);
}
/** @test */
@ -26,7 +26,7 @@ class RoleDefinitionTest extends TestCase
$domain = Domain::withAuthority('foo.com')->setId('123');
$definition = RoleDefinition::forDomain($domain);
self::assertEquals(Role::DOMAIN_SPECIFIC, $definition->roleName());
self::assertEquals(['domain_id' => '123', 'authority' => 'foo.com'], $definition->meta());
self::assertEquals(Role::DOMAIN_SPECIFIC, $definition->roleName);
self::assertEquals(['domain_id' => '123', 'authority' => 'foo.com'], $definition->meta);
}
}

View file

@ -46,7 +46,7 @@ class ApiKeyServiceTest extends TestCase
self::assertEquals($date, $key->getExpirationDate());
self::assertEquals($name, $key->name());
foreach ($roles as $roleDefinition) {
self::assertTrue($key->hasRole($roleDefinition->roleName()));
self::assertTrue($key->hasRole($roleDefinition->roleName));
}
}
@ -77,7 +77,7 @@ class ApiKeyServiceTest extends TestCase
$result = $this->service->check('12345');
self::assertFalse($result->isValid());
self::assertSame($invalidKey, $result->apiKey());
self::assertSame($invalidKey, $result->apiKey);
}
public function provideInvalidApiKeys(): iterable
@ -100,7 +100,7 @@ class ApiKeyServiceTest extends TestCase
$result = $this->service->check('12345');
self::assertTrue($result->isValid());
self::assertSame($apiKey, $result->apiKey());
self::assertSame($apiKey, $result->apiKey);
}
/** @test */