ShortUrl.php 7.2 KB

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