diff --git a/module/Core/src/Entity/ShortUrl.php b/module/Core/src/Entity/ShortUrl.php
index aba0235f..ea56fc82 100644
--- a/module/Core/src/Entity/ShortUrl.php
+++ b/module/Core/src/Entity/ShortUrl.php
@@ -135,8 +135,8 @@ class ShortUrl extends AbstractEntity
*/
public function regenerateShortCode(): self
{
- // In ShortUrls where a custom slug was provided, do nothing
- if ($this->customSlugWasProvided) {
+ // In ShortUrls where a custom slug was provided, throw error, unless it is an imported one
+ if ($this->customSlugWasProvided && $this->importSource === null) {
throw ShortCodeCannotBeRegeneratedException::forShortUrlWithCustomSlug();
}
diff --git a/module/Core/src/Importer/ImportedLinksProcessor.php b/module/Core/src/Importer/ImportedLinksProcessor.php
index 81b8e923..4dc8d1a4 100644
--- a/module/Core/src/Importer/ImportedLinksProcessor.php
+++ b/module/Core/src/Importer/ImportedLinksProcessor.php
@@ -7,12 +7,14 @@ namespace Shlinkio\Shlink\Core\Importer;
use Doctrine\ORM\EntityManagerInterface;
use Shlinkio\Shlink\Core\Domain\Resolver\DomainResolverInterface;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
+use Shlinkio\Shlink\Core\Repository\ShortUrlRepository;
use Shlinkio\Shlink\Core\Repository\ShortUrlRepositoryInterface;
use Shlinkio\Shlink\Core\Util\DoctrineBatchIterator;
use Shlinkio\Shlink\Core\Util\TagManagerTrait;
use Shlinkio\Shlink\Importer\ImportedLinksProcessorInterface;
use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
use Symfony\Component\Console\Style\StyleInterface;
+
use function sprintf;
class ImportedLinksProcessor implements ImportedLinksProcessorInterface
@@ -40,21 +42,71 @@ class ImportedLinksProcessor implements ImportedLinksProcessorInterface
/** @var ImportedShlinkUrl $url */
foreach ($iterable as $url) {
+ $longUrl = $url->longUrl();
+
// Skip already imported URLs
if ($shortUrlRepo->importedUrlExists($url, $importShortCodes)) {
- $io->text(sprintf('%s: Skipped', $url->longUrl()));
+ $io->text(sprintf('%s: Skipped', $longUrl));
continue;
}
$shortUrl = ShortUrl::fromImport($url, $importShortCodes, $this->domainResolver);
$shortUrl->setTags($this->tagNamesToEntities($this->em, $url->tags()));
+ if (! $this->handleShortcodeUniqueness($url, $shortUrl, $io, $importShortCodes)) {
+ continue;
+ }
- // TODO Handle errors while creating short URLs, to avoid making the whole process fail
- // * Duplicated short code
$this->em->persist($shortUrl);
-
- $io->text(sprintf('%s: Imported', $url->longUrl()));
+ $io->text(sprintf('%s: Imported', $longUrl));
}
}
+
+ private function handleShortcodeUniqueness(
+ ImportedShlinkUrl $url,
+ ShortUrl $shortUrl,
+ StyleInterface $io,
+ bool $importShortCodes
+ ): bool {
+ if ($this->ensureShortCodeUniqueness($shortUrl, $importShortCodes)) {
+ return true;
+ }
+
+ $longUrl = $url->longUrl();
+ $action = $io->choice(sprintf(
+ 'Failed to import URL "%s" because its short-code "%s" is already in use. Do you want to generate a new '
+ . 'one or skip it?',
+ $longUrl,
+ $url->shortCode(),
+ ), ['Generate new short-code', 'Skip'], 1);
+
+ if ($action === 'Skip') {
+ $io->text(sprintf('%s: Skipped', $longUrl));
+ return false;
+ }
+
+ return $this->handleShortcodeUniqueness($url, $shortUrl, $io, false);
+ }
+
+ private function ensureShortCodeUniqueness(ShortUrl $shortUrlToBeCreated, bool $hasCustomSlug): bool
+ {
+ $shortCode = $shortUrlToBeCreated->getShortCode();
+ $domain = $shortUrlToBeCreated->getDomain();
+ $domainAuthority = $domain !== null ? $domain->getAuthority() : null;
+
+ /** @var ShortUrlRepository $repo */
+ $repo = $this->em->getRepository(ShortUrl::class);
+ $otherShortUrlsExist = $repo->shortCodeIsInUse($shortCode, $domainAuthority);
+
+ if (! $otherShortUrlsExist) {
+ return true;
+ }
+
+ if ($hasCustomSlug) {
+ return false;
+ }
+
+ $shortUrlToBeCreated->regenerateShortCode();
+ return $this->ensureShortCodeUniqueness($shortUrlToBeCreated, $hasCustomSlug);
+ }
}
diff --git a/module/Core/test/Entity/ShortUrlTest.php b/module/Core/test/Entity/ShortUrlTest.php
index 054182ff..c143ae76 100644
--- a/module/Core/test/Entity/ShortUrlTest.php
+++ b/module/Core/test/Entity/ShortUrlTest.php
@@ -4,12 +4,14 @@ declare(strict_types=1);
namespace ShlinkioTest\Shlink\Core\Entity;
+use Cake\Chronos\Chronos;
use PHPUnit\Framework\TestCase;
use Shlinkio\Shlink\Core\Entity\ShortUrl;
use Shlinkio\Shlink\Core\Exception\ShortCodeCannotBeRegeneratedException;
use Shlinkio\Shlink\Core\Model\ShortUrlMeta;
use Shlinkio\Shlink\Core\Validation\ShortUrlMetaInputFilter;
+use Shlinkio\Shlink\Importer\Model\ImportedShlinkUrl;
use function Functional\map;
use function range;
use function strlen;
@@ -44,10 +46,12 @@ class ShortUrlTest extends TestCase
];
}
- /** @test */
- public function regenerateShortCodeProperlyChangesTheValueOnValidShortUrls(): void
+ /**
+ * @test
+ * @dataProvider provideValidShortUrls
+ */
+ public function regenerateShortCodeProperlyChangesTheValueOnValidShortUrls(ShortUrl $shortUrl): void
{
- $shortUrl = new ShortUrl('');
$firstShortCode = $shortUrl->getShortCode();
$shortUrl->regenerateShortCode();
@@ -56,6 +60,14 @@ class ShortUrlTest extends TestCase
self::assertNotEquals($firstShortCode, $secondShortCode);
}
+ public function provideValidShortUrls(): iterable
+ {
+ yield 'no custom slug' => [new ShortUrl('')];
+ yield 'imported with custom slug' => [
+ ShortUrl::fromImport(new ImportedShlinkUrl('', '', [], Chronos::now(), null, 'custom-slug'), true),
+ ];
+ }
+
/**
* @test
* @dataProvider provideLengths