ShortUrl.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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\Exception\ShortCodeCannotBeRegeneratedException;
  10. use Shlinkio\Shlink\Core\Model\ShortUrlEdit;
  11. use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
  12. use Shlinkio\Shlink\Core\ShortUrl\Resolver\ShortUrlRelationResolverInterface;
  13. use Shlinkio\Shlink\Core\ShortUrl\Resolver\SimpleShortUrlRelationResolver;
  14. use Shlinkio\Shlink\Core\Validation\ShortUrlInputFilter;
  15. use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
  16. use Shlinkio\Shlink\Rest\Entity\ApiKey;
  17. use function count;
  18. use function Shlinkio\Shlink\Core\generateRandomShortCode;
  19. class ShortUrl extends AbstractEntity
  20. {
  21. private string $longUrl;
  22. private string $shortCode;
  23. private Chronos $dateCreated;
  24. /** @var Collection|Visit[] */
  25. private Collection $visits;
  26. /** @var Collection|Tag[] */
  27. private Collection $tags;
  28. private ?Chronos $validSince = null;
  29. private ?Chronos $validUntil = null;
  30. private ?int $maxVisits = null;
  31. private ?Domain $domain = null;
  32. private bool $customSlugWasProvided;
  33. private int $shortCodeLength;
  34. private ?string $importSource = null;
  35. private ?string $importOriginalShortCode = null;
  36. private ?ApiKey $authorApiKey = null;
  37. private function __construct()
  38. {
  39. }
  40. public static function createEmpty(): self
  41. {
  42. return self::fromMeta(ShortUrlMeta::createEmpty());
  43. }
  44. public static function withLongUrl(string $longUrl): self
  45. {
  46. return self::fromMeta(ShortUrlMeta::fromRawData([ShortUrlInputFilter::LONG_URL => $longUrl]));
  47. }
  48. public static function fromMeta(
  49. ShortUrlMeta $meta,
  50. ?ShortUrlRelationResolverInterface $relationResolver = null
  51. ): self {
  52. $instance = new self();
  53. $relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver();
  54. $instance->longUrl = $meta->getLongUrl();
  55. $instance->dateCreated = Chronos::now();
  56. $instance->visits = new ArrayCollection();
  57. $instance->tags = $relationResolver->resolveTags($meta->getTags());
  58. $instance->validSince = $meta->getValidSince();
  59. $instance->validUntil = $meta->getValidUntil();
  60. $instance->maxVisits = $meta->getMaxVisits();
  61. $instance->customSlugWasProvided = $meta->hasCustomSlug();
  62. $instance->shortCodeLength = $meta->getShortCodeLength();
  63. $instance->shortCode = $meta->getCustomSlug() ?? generateRandomShortCode($instance->shortCodeLength);
  64. $instance->domain = $relationResolver->resolveDomain($meta->getDomain());
  65. $instance->authorApiKey = $meta->getApiKey();
  66. return $instance;
  67. }
  68. public static function fromImport(
  69. ImportedShlinkUrl $url,
  70. bool $importShortCode,
  71. ?ShortUrlRelationResolverInterface $relationResolver = null
  72. ): self {
  73. $meta = [
  74. ShortUrlInputFilter::LONG_URL => $url->longUrl(),
  75. ShortUrlInputFilter::DOMAIN => $url->domain(),
  76. ShortUrlInputFilter::TAGS => $url->tags(),
  77. ShortUrlInputFilter::VALIDATE_URL => false,
  78. ];
  79. if ($importShortCode) {
  80. $meta[ShortUrlInputFilter::CUSTOM_SLUG] = $url->shortCode();
  81. }
  82. $instance = self::fromMeta(ShortUrlMeta::fromRawData($meta), $relationResolver);
  83. $instance->importSource = $url->source();
  84. $instance->importOriginalShortCode = $url->shortCode();
  85. $instance->dateCreated = Chronos::instance($url->createdAt());
  86. return $instance;
  87. }
  88. public function getLongUrl(): string
  89. {
  90. return $this->longUrl;
  91. }
  92. public function getShortCode(): string
  93. {
  94. return $this->shortCode;
  95. }
  96. public function getDateCreated(): Chronos
  97. {
  98. return $this->dateCreated;
  99. }
  100. public function getDomain(): ?Domain
  101. {
  102. return $this->domain;
  103. }
  104. /**
  105. * @return Collection|Tag[]
  106. */
  107. public function getTags(): Collection
  108. {
  109. return $this->tags;
  110. }
  111. public function update(
  112. ShortUrlEdit $shortUrlEdit,
  113. ?ShortUrlRelationResolverInterface $relationResolver = null
  114. ): void {
  115. if ($shortUrlEdit->hasValidSince()) {
  116. $this->validSince = $shortUrlEdit->validSince();
  117. }
  118. if ($shortUrlEdit->hasValidUntil()) {
  119. $this->validUntil = $shortUrlEdit->validUntil();
  120. }
  121. if ($shortUrlEdit->hasMaxVisits()) {
  122. $this->maxVisits = $shortUrlEdit->maxVisits();
  123. }
  124. if ($shortUrlEdit->hasLongUrl()) {
  125. $this->longUrl = $shortUrlEdit->longUrl();
  126. }
  127. if ($shortUrlEdit->hasTags()) {
  128. $relationResolver = $relationResolver ?? new SimpleShortUrlRelationResolver();
  129. $this->tags = $relationResolver->resolveTags($shortUrlEdit->tags());
  130. }
  131. }
  132. /**
  133. * @throws ShortCodeCannotBeRegeneratedException
  134. */
  135. public function regenerateShortCode(): void
  136. {
  137. // In ShortUrls where a custom slug was provided, throw error, unless it is an imported one
  138. if ($this->customSlugWasProvided && $this->importSource === null) {
  139. throw ShortCodeCannotBeRegeneratedException::forShortUrlWithCustomSlug();
  140. }
  141. // The short code can be regenerated only on ShortUrl which have not been persisted yet
  142. if ($this->id !== null) {
  143. throw ShortCodeCannotBeRegeneratedException::forShortUrlAlreadyPersisted();
  144. }
  145. $this->shortCode = generateRandomShortCode($this->shortCodeLength);
  146. }
  147. public function getValidSince(): ?Chronos
  148. {
  149. return $this->validSince;
  150. }
  151. public function getValidUntil(): ?Chronos
  152. {
  153. return $this->validUntil;
  154. }
  155. public function getVisitsCount(): int
  156. {
  157. return count($this->visits);
  158. }
  159. /**
  160. * @param Collection|Visit[] $visits
  161. * @internal
  162. */
  163. public function setVisits(Collection $visits): self
  164. {
  165. $this->visits = $visits;
  166. return $this;
  167. }
  168. public function getMaxVisits(): ?int
  169. {
  170. return $this->maxVisits;
  171. }
  172. public function isEnabled(): bool
  173. {
  174. $maxVisitsReached = $this->maxVisits !== null && $this->getVisitsCount() >= $this->maxVisits;
  175. if ($maxVisitsReached) {
  176. return false;
  177. }
  178. $now = Chronos::now();
  179. $beforeValidSince = $this->validSince !== null && $this->validSince->gt($now);
  180. if ($beforeValidSince) {
  181. return false;
  182. }
  183. $afterValidUntil = $this->validUntil !== null && $this->validUntil->lt($now);
  184. if ($afterValidUntil) {
  185. return false;
  186. }
  187. return true;
  188. }
  189. public function toString(array $domainConfig): string
  190. {
  191. return (new Uri())->withPath($this->shortCode)
  192. ->withScheme($domainConfig['schema'] ?? 'http')
  193. ->withHost($this->resolveDomain($domainConfig['hostname'] ?? ''))
  194. ->__toString();
  195. }
  196. private function resolveDomain(string $fallback = ''): string
  197. {
  198. if ($this->domain === null) {
  199. return $fallback;
  200. }
  201. return $this->domain->getAuthority();
  202. }
  203. }