Refactored global models into their own proper namespaces

This commit is contained in:
Alejandro Celaya
2022-09-23 18:05:17 +02:00
parent 1e3ccba503
commit f5f990511c
125 changed files with 303 additions and 300 deletions

View File

@@ -1,194 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\ShortUrl\Helper\TitleResolutionModelInterface;
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
use function array_key_exists;
use function Shlinkio\Shlink\Core\getOptionalBoolFromInputFilter;
use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter;
use function Shlinkio\Shlink\Core\normalizeDate;
// TODO Rename to ShortUrlEdition
final class ShortUrlEdit implements TitleResolutionModelInterface
{
private bool $longUrlPropWasProvided = false;
private ?string $longUrl = null;
private bool $validSincePropWasProvided = false;
private ?Chronos $validSince = null;
private bool $validUntilPropWasProvided = false;
private ?Chronos $validUntil = null;
private bool $maxVisitsPropWasProvided = false;
private ?int $maxVisits = null;
private bool $tagsPropWasProvided = false;
private array $tags = [];
private bool $titlePropWasProvided = false;
private ?string $title = null;
private bool $titleWasAutoResolved = false;
private bool $validateUrl = false;
private bool $crawlablePropWasProvided = false;
private bool $crawlable = false;
private bool $forwardQueryPropWasProvided = false;
private bool $forwardQuery = true;
private function __construct()
{
}
/**
* @throws ValidationException
*/
public static function fromRawData(array $data): self
{
$instance = new self();
$instance->validateAndInit($data);
return $instance;
}
/**
* @throws ValidationException
*/
private function validateAndInit(array $data): void
{
$inputFilter = ShortUrlInputFilter::withNonRequiredLongUrl($data);
if (! $inputFilter->isValid()) {
throw ValidationException::fromInputFilter($inputFilter);
}
$this->longUrlPropWasProvided = array_key_exists(ShortUrlInputFilter::LONG_URL, $data);
$this->validSincePropWasProvided = array_key_exists(ShortUrlInputFilter::VALID_SINCE, $data);
$this->validUntilPropWasProvided = array_key_exists(ShortUrlInputFilter::VALID_UNTIL, $data);
$this->maxVisitsPropWasProvided = array_key_exists(ShortUrlInputFilter::MAX_VISITS, $data);
$this->tagsPropWasProvided = array_key_exists(ShortUrlInputFilter::TAGS, $data);
$this->titlePropWasProvided = array_key_exists(ShortUrlInputFilter::TITLE, $data);
$this->crawlablePropWasProvided = array_key_exists(ShortUrlInputFilter::CRAWLABLE, $data);
$this->forwardQueryPropWasProvided = array_key_exists(ShortUrlInputFilter::FORWARD_QUERY, $data);
$this->longUrl = $inputFilter->getValue(ShortUrlInputFilter::LONG_URL);
$this->validSince = normalizeDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE));
$this->validUntil = normalizeDate($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL));
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlInputFilter::MAX_VISITS);
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL) ?? false;
$this->tags = $inputFilter->getValue(ShortUrlInputFilter::TAGS);
$this->title = $inputFilter->getValue(ShortUrlInputFilter::TITLE);
$this->crawlable = $inputFilter->getValue(ShortUrlInputFilter::CRAWLABLE);
$this->forwardQuery = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::FORWARD_QUERY) ?? true;
}
public function longUrl(): ?string
{
return $this->longUrl;
}
public function getLongUrl(): string
{
return $this->longUrl() ?? '';
}
public function longUrlWasProvided(): bool
{
return $this->longUrlPropWasProvided && $this->longUrl !== null;
}
public function validSince(): ?Chronos
{
return $this->validSince;
}
public function validSinceWasProvided(): bool
{
return $this->validSincePropWasProvided;
}
public function validUntil(): ?Chronos
{
return $this->validUntil;
}
public function validUntilWasProvided(): bool
{
return $this->validUntilPropWasProvided;
}
public function maxVisits(): ?int
{
return $this->maxVisits;
}
public function maxVisitsWasProvided(): bool
{
return $this->maxVisitsPropWasProvided;
}
/**
* @return string[]
*/
public function tags(): array
{
return $this->tags;
}
public function tagsWereProvided(): bool
{
return $this->tagsPropWasProvided;
}
public function title(): ?string
{
return $this->title;
}
public function titleWasProvided(): bool
{
return $this->titlePropWasProvided;
}
public function hasTitle(): bool
{
return $this->titleWasProvided();
}
public function titleWasAutoResolved(): bool
{
return $this->titleWasAutoResolved;
}
public function withResolvedTitle(string $title): self
{
$copy = clone $this;
$copy->title = $title;
$copy->titleWasAutoResolved = true;
return $copy;
}
public function doValidateUrl(): bool
{
return $this->validateUrl;
}
public function crawlable(): bool
{
return $this->crawlable;
}
public function crawlableWasProvided(): bool
{
return $this->crawlablePropWasProvided;
}
public function forwardQuery(): bool
{
return $this->forwardQuery;
}
public function forwardQueryWasProvided(): bool
{
return $this->forwardQueryPropWasProvided;
}
}

View File

@@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Symfony\Component\Console\Input\InputInterface;
final class ShortUrlIdentifier
{
private function __construct(public readonly string $shortCode, public readonly ?string $domain = null)
{
}
public static function fromApiRequest(ServerRequestInterface $request): self
{
$shortCode = $request->getAttribute('shortCode', '');
$domain = $request->getQueryParams()['domain'] ?? null;
return new self($shortCode, $domain);
}
public static function fromRedirectRequest(ServerRequestInterface $request): self
{
$shortCode = $request->getAttribute('shortCode', '');
$domain = $request->getUri()->getAuthority();
return new self($shortCode, $domain);
}
public static function fromCli(InputInterface $input): self
{
// Using getArguments and getOptions instead of getArgument(...) and getOption(...) because
// the later throw an exception if requested options are not defined
/** @var string $shortCode */
$shortCode = $input->getArguments()['shortCode'] ?? '';
/** @var string|null $domain */
$domain = $input->getOptions()['domain'] ?? null;
return new self($shortCode, $domain);
}
public static function fromShortUrl(ShortUrl $shortUrl): self
{
$domain = $shortUrl->getDomain();
$domainAuthority = $domain?->getAuthority();
return new self($shortUrl->getShortCode(), $domainAuthority);
}
public static function fromShortCodeAndDomain(string $shortCode, ?string $domain = null): self
{
return new self($shortCode, $domain);
}
}

View File

@@ -1,206 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\ShortUrl\Helper\TitleResolutionModelInterface;
use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use function Shlinkio\Shlink\Core\getOptionalBoolFromInputFilter;
use function Shlinkio\Shlink\Core\getOptionalIntFromInputFilter;
use function Shlinkio\Shlink\Core\normalizeDate;
use const Shlinkio\Shlink\DEFAULT_SHORT_CODES_LENGTH;
// TODO Rename to ShortUrlCreation
final class ShortUrlMeta implements TitleResolutionModelInterface
{
private string $longUrl;
private ?Chronos $validSince = null;
private ?Chronos $validUntil = null;
private ?string $customSlug = null;
private ?int $maxVisits = null;
private ?bool $findIfExists = null;
private ?string $domain = null;
private int $shortCodeLength = 5;
private bool $validateUrl = false;
private ?ApiKey $apiKey = null;
private array $tags = [];
private ?string $title = null;
private bool $titleWasAutoResolved = false;
private bool $crawlable = false;
private bool $forwardQuery = true;
private function __construct()
{
}
public static function createEmpty(): self
{
$instance = new self();
$instance->longUrl = '';
return $instance;
}
/**
* @throws ValidationException
*/
public static function fromRawData(array $data): self
{
$instance = new self();
$instance->validateAndInit($data);
return $instance;
}
/**
* @throws ValidationException
*/
private function validateAndInit(array $data): void
{
$inputFilter = ShortUrlInputFilter::withRequiredLongUrl($data);
if (! $inputFilter->isValid()) {
throw ValidationException::fromInputFilter($inputFilter);
}
$this->longUrl = $inputFilter->getValue(ShortUrlInputFilter::LONG_URL);
$this->validSince = normalizeDate($inputFilter->getValue(ShortUrlInputFilter::VALID_SINCE));
$this->validUntil = normalizeDate($inputFilter->getValue(ShortUrlInputFilter::VALID_UNTIL));
$this->customSlug = $inputFilter->getValue(ShortUrlInputFilter::CUSTOM_SLUG);
$this->maxVisits = getOptionalIntFromInputFilter($inputFilter, ShortUrlInputFilter::MAX_VISITS);
$this->findIfExists = $inputFilter->getValue(ShortUrlInputFilter::FIND_IF_EXISTS);
$this->validateUrl = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::VALIDATE_URL) ?? false;
$this->domain = $inputFilter->getValue(ShortUrlInputFilter::DOMAIN);
$this->shortCodeLength = getOptionalIntFromInputFilter(
$inputFilter,
ShortUrlInputFilter::SHORT_CODE_LENGTH,
) ?? DEFAULT_SHORT_CODES_LENGTH;
$this->apiKey = $inputFilter->getValue(ShortUrlInputFilter::API_KEY);
$this->tags = $inputFilter->getValue(ShortUrlInputFilter::TAGS);
$this->title = $inputFilter->getValue(ShortUrlInputFilter::TITLE);
$this->crawlable = $inputFilter->getValue(ShortUrlInputFilter::CRAWLABLE);
$this->forwardQuery = getOptionalBoolFromInputFilter($inputFilter, ShortUrlInputFilter::FORWARD_QUERY) ?? true;
}
public function getLongUrl(): string
{
return $this->longUrl;
}
public function getValidSince(): ?Chronos
{
return $this->validSince;
}
public function hasValidSince(): bool
{
return $this->validSince !== null;
}
public function getValidUntil(): ?Chronos
{
return $this->validUntil;
}
public function hasValidUntil(): bool
{
return $this->validUntil !== null;
}
public function getCustomSlug(): ?string
{
return $this->customSlug;
}
public function hasCustomSlug(): bool
{
return $this->customSlug !== null;
}
public function getMaxVisits(): ?int
{
return $this->maxVisits;
}
public function hasMaxVisits(): bool
{
return $this->maxVisits !== null;
}
public function findIfExists(): bool
{
return (bool) $this->findIfExists;
}
public function hasDomain(): bool
{
return $this->domain !== null;
}
public function getDomain(): ?string
{
return $this->domain;
}
public function getShortCodeLength(): int
{
return $this->shortCodeLength;
}
public function doValidateUrl(): bool
{
return $this->validateUrl;
}
public function getApiKey(): ?ApiKey
{
return $this->apiKey;
}
/**
* @return string[]
*/
public function getTags(): array
{
return $this->tags;
}
public function getTitle(): ?string
{
return $this->title;
}
public function hasTitle(): bool
{
return $this->title !== null;
}
public function titleWasAutoResolved(): bool
{
return $this->titleWasAutoResolved;
}
public function withResolvedTitle(string $title): self
{
$copy = clone $this;
$copy->title = $title;
$copy->titleWasAutoResolved = true;
return $copy;
}
public function isCrawlable(): bool
{
return $this->crawlable;
}
public function forwardQuery(): bool
{
return $this->forwardQuery;
}
}

View File

@@ -1,115 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Exception\ValidationException;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Core\Validation\ShortUrlsParamsInputFilter;
use function Shlinkio\Shlink\Common\buildDateRange;
use function Shlinkio\Shlink\Core\normalizeDate;
final class ShortUrlsParams
{
public const ORDERABLE_FIELDS = ['longUrl', 'shortCode', 'dateCreated', 'title', 'visits'];
public const DEFAULT_ITEMS_PER_PAGE = 10;
private int $page;
private int $itemsPerPage;
private ?string $searchTerm;
private array $tags;
private TagsMode $tagsMode = TagsMode::ANY;
private Ordering $orderBy;
private ?DateRange $dateRange;
private function __construct()
{
}
public static function emptyInstance(): self
{
return self::fromRawData([]);
}
/**
* @throws ValidationException
*/
public static function fromRawData(array $query): self
{
$instance = new self();
$instance->validateAndInit($query);
return $instance;
}
/**
* @throws ValidationException
*/
private function validateAndInit(array $query): void
{
$inputFilter = new ShortUrlsParamsInputFilter($query);
if (! $inputFilter->isValid()) {
throw ValidationException::fromInputFilter($inputFilter);
}
$this->page = (int) ($inputFilter->getValue(ShortUrlsParamsInputFilter::PAGE) ?? 1);
$this->searchTerm = $inputFilter->getValue(ShortUrlsParamsInputFilter::SEARCH_TERM);
$this->tags = (array) $inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS);
$this->dateRange = buildDateRange(
normalizeDate($inputFilter->getValue(ShortUrlsParamsInputFilter::START_DATE)),
normalizeDate($inputFilter->getValue(ShortUrlsParamsInputFilter::END_DATE)),
);
$this->orderBy = Ordering::fromTuple($inputFilter->getValue(ShortUrlsParamsInputFilter::ORDER_BY));
$this->itemsPerPage = (int) (
$inputFilter->getValue(ShortUrlsParamsInputFilter::ITEMS_PER_PAGE) ?? self::DEFAULT_ITEMS_PER_PAGE
);
$this->tagsMode = $this->resolveTagsMode($inputFilter->getValue(ShortUrlsParamsInputFilter::TAGS_MODE));
}
private function resolveTagsMode(?string $rawTagsMode): TagsMode
{
if ($rawTagsMode === null) {
return TagsMode::ANY;
}
return TagsMode::tryFrom($rawTagsMode) ?? TagsMode::ANY;
}
public function page(): int
{
return $this->page;
}
public function itemsPerPage(): int
{
return $this->itemsPerPage;
}
public function searchTerm(): ?string
{
return $this->searchTerm;
}
public function tags(): array
{
return $this->tags;
}
public function orderBy(): Ordering
{
return $this->orderBy;
}
public function dateRange(): ?DateRange
{
return $this->dateRange;
}
public function tagsMode(): TagsMode
{
return $this->tagsMode;
}
}

View File

@@ -1,83 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Psr\Http\Message\ServerRequestInterface;
use Shlinkio\Shlink\Common\Middleware\IpAddressMiddlewareFactory;
use Shlinkio\Shlink\Core\Options\TrackingOptions;
use function Shlinkio\Shlink\Core\isCrawler;
use function substr;
final class Visitor
{
public const USER_AGENT_MAX_LENGTH = 512;
public const REFERER_MAX_LENGTH = 1024;
public const REMOTE_ADDRESS_MAX_LENGTH = 256;
public const VISITED_URL_MAX_LENGTH = 2048;
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)
{
$this->userAgent = $this->cropToLength($userAgent, self::USER_AGENT_MAX_LENGTH);
$this->referer = $this->cropToLength($referer, self::REFERER_MAX_LENGTH);
$this->visitedUrl = $this->cropToLength($visitedUrl, self::VISITED_URL_MAX_LENGTH);
$this->remoteAddress = $remoteAddress === null ? null : $this->cropToLength(
$remoteAddress,
self::REMOTE_ADDRESS_MAX_LENGTH,
);
$this->potentialBot = isCrawler($userAgent);
}
private function cropToLength(string $value, int $length): string
{
return substr($value, 0, $length);
}
public static function fromRequest(ServerRequestInterface $request): self
{
return new self(
$request->getHeaderLine('User-Agent'),
$request->getHeaderLine('Referer'),
$request->getAttribute(IpAddressMiddlewareFactory::REQUEST_ATTR),
$request->getUri()->__toString(),
);
}
public static function emptyInstance(): self
{
return new self('', '', null, '');
}
public static function botInstance(): self
{
return new self('cf-facebook', '', null, '');
}
public function isPotentialBot(): bool
{
return $this->potentialBot;
}
public function normalizeForTrackingOptions(TrackingOptions $options): self
{
$instance = new self(
$options->disableUaTracking ? '' : $this->userAgent,
$options->disableReferrerTracking ? '' : $this->referer,
$options->disableIpTracking ? null : $this->remoteAddress,
$this->visitedUrl,
);
// Keep the fact that the visit was a potential bot, even if we no longer save the user agent
$instance->potentialBot = $this->potentialBot;
return $instance;
}
}

View File

@@ -1,34 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Model;
use Shlinkio\Shlink\Common\Util\DateRange;
use function Shlinkio\Shlink\Core\parseDateRangeFromQuery;
final class VisitsParams extends AbstractInfinitePaginableListParams
{
public readonly DateRange $dateRange;
public function __construct(
?DateRange $dateRange = null,
?int $page = null,
?int $itemsPerPage = null,
public readonly bool $excludeBots = false,
) {
parent::__construct($page, $itemsPerPage);
$this->dateRange = $dateRange ?? DateRange::allTime();
}
public static function fromRawData(array $query): self
{
return new self(
parseDateRangeFromQuery($query, 'startDate', 'endDate'),
isset($query['page']) ? (int) $query['page'] : null,
isset($query['itemsPerPage']) ? (int) $query['itemsPerPage'] : null,
isset($query['excludeBots']),
);
}
}