ShortUrl.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <?php
  2. declare(strict_types=1);
  3. namespace Shlinkio\Shlink\Core\Entity;
  4. use Cake\Chronos\Chronos;
  5. use Doctrine\Common\Collections\ArrayCollection;
  6. use Doctrine\Common\Collections\Collection;
  7. use Laminas\Diactoros\Uri;
  8. use Shlinkio\Shlink\Common\Entity\AbstractEntity;
  9. use Shlinkio\Shlink\Core\Domain\Resolver\DomainResolverInterface;
  10. use Shlinkio\Shlink\Core\Domain\Resolver\SimpleDomainResolver;
  11. use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException;
  12. use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
  13. use function array_reduce;
  14. use function count;
  15. use function Functional\contains;
  16. use function Functional\invoke;
  17. use function Shlinkio\Shlink\Core\generateRandomShortCode;
  18. class ShortUrl extends AbstractEntity
  19. {
  20. private string $longUrl;
  21. private string $shortCode;
  22. private Chronos $dateCreated;
  23. /** @var Collection|Visit[] */
  24. private Collection $visits;
  25. /** @var Collection|Tag[] */
  26. private Collection $tags;
  27. private ?Chronos $validSince;
  28. private ?Chronos $validUntil;
  29. private ?int $maxVisits;
  30. private ?Domain $domain;
  31. private bool $customSlugWasProvided;
  32. public function __construct(
  33. string $longUrl,
  34. ?ShortUrlMeta $meta = null,
  35. ?DomainResolverInterface $domainResolver = null
  36. ) {
  37. $meta = $meta ?? ShortUrlMeta::createEmpty();
  38. $this->longUrl = $longUrl;
  39. $this->dateCreated = Chronos::now();
  40. $this->visits = new ArrayCollection();
  41. $this->tags = new ArrayCollection();
  42. $this->validSince = $meta->getValidSince();
  43. $this->validUntil = $meta->getValidUntil();
  44. $this->maxVisits = $meta->getMaxVisits();
  45. $this->customSlugWasProvided = $meta->hasCustomSlug();
  46. $this->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode();
  47. $this->domain = ($domainResolver ?? new SimpleDomainResolver())->resolveDomain($meta->getDomain());
  48. }
  49. public function getLongUrl(): string
  50. {
  51. return $this->longUrl;
  52. }
  53. public function getShortCode(): string
  54. {
  55. return $this->shortCode;
  56. }
  57. public function getDateCreated(): Chronos
  58. {
  59. return $this->dateCreated;
  60. }
  61. /**
  62. * @return Collection|Tag[]
  63. */
  64. public function getTags(): Collection
  65. {
  66. return $this->tags;
  67. }
  68. /**
  69. * @param Collection|Tag[] $tags
  70. */
  71. public function setTags(Collection $tags): self
  72. {
  73. $this->tags = $tags;
  74. return $this;
  75. }
  76. public function updateMeta(ShortUrlMeta $shortCodeMeta): void
  77. {
  78. if ($shortCodeMeta->hasValidSince()) {
  79. $this->validSince = $shortCodeMeta->getValidSince();
  80. }
  81. if ($shortCodeMeta->hasValidUntil()) {
  82. $this->validUntil = $shortCodeMeta->getValidUntil();
  83. }
  84. if ($shortCodeMeta->hasMaxVisits()) {
  85. $this->maxVisits = $shortCodeMeta->getMaxVisits();
  86. }
  87. }
  88. /**
  89. * @throws ShortCodeCannotBeRegeneratedException
  90. */
  91. public function regenerateShortCode(): self
  92. {
  93. // In ShortUrls where a custom slug was provided, do nothing
  94. if ($this->customSlugWasProvided) {
  95. throw ShortCodeCannotBeRegeneratedException::forShortUrlWithCustomSlug();
  96. }
  97. // The short code can be regenerated only on ShortUrl which have not been persisted yet
  98. if ($this->id !== null) {
  99. throw ShortCodeCannotBeRegeneratedException::forShortUrlAlreadyPersisted();
  100. }
  101. $this->shortCode = generateRandomShortCode();
  102. return $this;
  103. }
  104. public function getValidSince(): ?Chronos
  105. {
  106. return $this->validSince;
  107. }
  108. public function getValidUntil(): ?Chronos
  109. {
  110. return $this->validUntil;
  111. }
  112. public function getVisitsCount(): int
  113. {
  114. return count($this->visits);
  115. }
  116. /**
  117. * @param Collection|Visit[] $visits
  118. * @return ShortUrl
  119. * @internal
  120. */
  121. public function setVisits(Collection $visits): self
  122. {
  123. $this->visits = $visits;
  124. return $this;
  125. }
  126. public function getMaxVisits(): ?int
  127. {
  128. return $this->maxVisits;
  129. }
  130. public function maxVisitsReached(): bool
  131. {
  132. return $this->maxVisits !== null && $this->getVisitsCount() >= $this->maxVisits;
  133. }
  134. public function toString(array $domainConfig): string
  135. {
  136. return (string) (new Uri())->withPath($this->shortCode)
  137. ->withScheme($domainConfig['schema'] ?? 'http')
  138. ->withHost($this->resolveDomain($domainConfig['hostname'] ?? ''));
  139. }
  140. private function resolveDomain(string $fallback = ''): string
  141. {
  142. if ($this->domain === null) {
  143. return $fallback;
  144. }
  145. return $this->domain->getAuthority();
  146. }
  147. public function matchesCriteria(ShortUrlMeta $meta, array $tags): bool
  148. {
  149. if ($meta->hasMaxVisits() && $meta->getMaxVisits() !== $this->maxVisits) {
  150. return false;
  151. }
  152. if ($meta->hasDomain() && $meta->getDomain() !== $this->resolveDomain()) {
  153. return false;
  154. }
  155. if ($meta->hasValidSince() && ! $meta->getValidSince()->eq($this->validSince)) {
  156. return false;
  157. }
  158. if ($meta->hasValidUntil() && ! $meta->getValidUntil()->eq($this->validUntil)) {
  159. return false;
  160. }
  161. $shortUrlTags = invoke($this->getTags(), '__toString');
  162. $hasAllTags = count($shortUrlTags) === count($tags) && array_reduce(
  163. $tags,
  164. fn (bool $hasAllTags, string $tag) => $hasAllTags && contains($shortUrlTags, $tag),
  165. true,
  166. );
  167. return $hasAllTags;
  168. }
  169. }