Convert CreateShortUrlCommand into invokable command

This commit is contained in:
Alejandro Celaya
2025-12-15 09:27:52 +01:00
parent b7ae228a95
commit 965d191ce1
4 changed files with 153 additions and 88 deletions

View File

@@ -4,109 +4,42 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Input\ShortUrlDataInput;
use Shlinkio\Shlink\CLI\Command\ShortUrl\Input\ShortUrlCreationInput;
use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\ShortUrl\UrlShortenerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\MapInput;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use function sprintf;
#[AsCommand(
name: CreateShortUrlCommand::NAME,
description: 'Generates a short URL for provided long URL and returns it',
)]
class CreateShortUrlCommand extends Command
{
public const string NAME = 'short-url:create';
private SymfonyStyle $io;
private readonly ShortUrlDataInput $shortUrlDataInput;
public function __construct(
private readonly UrlShortenerInterface $urlShortener,
private readonly ShortUrlStringifierInterface $stringifier,
private readonly UrlShortenerOptions $options,
) {
parent::__construct();
$this->shortUrlDataInput = new ShortUrlDataInput($this);
}
protected function configure(): void
public function __invoke(SymfonyStyle $io, #[MapInput] ShortUrlCreationInput $inputData): int
{
$this
->setName(self::NAME)
->setDescription('Generates a short URL for provided long URL and returns it')
->addOption(
'domain',
'd',
InputOption::VALUE_REQUIRED,
'The domain to which this short URL will be attached.',
)
->addOption(
'custom-slug',
'c',
InputOption::VALUE_REQUIRED,
'If provided, this slug will be used instead of generating a short code',
)
->addOption(
'short-code-length',
'l',
InputOption::VALUE_REQUIRED,
'The length for generated short code (it will be ignored if --custom-slug was provided).',
)
->addOption(
'path-prefix',
'p',
InputOption::VALUE_REQUIRED,
'Prefix to prepend before the generated short code or provided custom slug',
)
->addOption(
'find-if-exists',
'f',
InputOption::VALUE_NONE,
'This will force existing matching URL to be returned if found, instead of creating a new one.',
);
}
protected function interact(InputInterface $input, OutputInterface $output): void
{
$this->verifyLongUrlArgument($input, $output);
}
private function verifyLongUrlArgument(InputInterface $input, OutputInterface $output): void
{
$longUrl = $input->getArgument('longUrl');
if (! empty($longUrl)) {
return;
}
$io = $this->getIO($input, $output);
$longUrl = $io->ask('Which URL do you want to shorten?');
if (! empty($longUrl)) {
$input->setArgument('longUrl', $longUrl);
}
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = $this->getIO($input, $output);
try {
$result = $this->urlShortener->shorten($this->shortUrlDataInput->toShortUrlCreation(
$input,
$this->options,
customSlugField: 'custom-slug',
shortCodeLengthField: 'short-code-length',
pathPrefixField: 'path-prefix',
findIfExistsField: 'find-if-exists',
domainField: 'domain',
));
$result = $this->urlShortener->shorten($inputData->toShortUrlCreation($this->options));
$result->onEventDispatchingError(static fn () => $io->isVerbose() && $io->warning(
'Short URL properly created, but the real-time updates cannot be notified when generating the '
. 'short URL from the command line. Migrate to roadrunner in order to bypass this limitation.',
. 'short URL from the command line. Migrate to roadrunner in order to bypass this limitation.',
));
$io->writeln([
@@ -119,9 +52,4 @@ class CreateShortUrlCommand extends Command
return self::FAILURE;
}
}
private function getIO(InputInterface $input, OutputInterface $output): SymfonyStyle
{
return $this->io ??= new SymfonyStyle($input, $output);
}
}

View File

@@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl\Input;
use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\Ask;
use Symfony\Component\Console\Attribute\MapInput;
use Symfony\Component\Console\Attribute\Option;
/**
* Data used for short URL creation
*/
final class ShortUrlCreationInput
{
#[Argument('The long URL to set'), Ask('Which URL do you want to shorten?')]
public string $longUrl;
#[MapInput]
public ShortUrlDataInput $commonData;
#[Option('The domain to which this short URL will be attached', shortcut: 'd')]
public string|null $domain = null;
#[Option('If provided, this slug will be used instead of generating a short code', shortcut: 'c')]
public string|null $customSlug = null;
#[Option('The length for generated short code (it will be ignored if --custom-slug was provided)', shortcut: 'l')]
public int|null $shortCodeLength = null;
#[Option('Prefix to prepend before the generated short code or provided custom slug', shortcut: 'p')]
public string|null $pathPrefix = null;
#[Option(
'This will force existing matching URL to be returned if found, instead of creating a new one',
shortcut: 'f',
)]
public bool $findIfExists = false;
public function toShortUrlCreation(UrlShortenerOptions $options): ShortUrlCreation
{
$shortCodeLength = $this->shortCodeLength ?? $options->defaultShortCodesLength;
return ShortUrlCreation::fromRawData([
ShortUrlInputFilter::LONG_URL => $this->longUrl,
ShortUrlInputFilter::DOMAIN => $this->domain,
ShortUrlInputFilter::CUSTOM_SLUG => $this->customSlug,
ShortUrlInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
ShortUrlInputFilter::PATH_PREFIX => $this->pathPrefix,
ShortUrlInputFilter::FIND_IF_EXISTS => $this->findIfExists,
...$this->commonData->toArray(),
], $options);
}
}

View File

@@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl\Input;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
use Symfony\Component\Console\Attribute\Option;
use function array_unique;
/**
* Common input used for short URL creation and edition
*/
final class ShortUrlDataInput
{
/** @var string[]|null */
#[Option('Tags to apply to the short URL', name: 'tag', shortcut: 't')]
public array|null $tags = null;
#[Option(
'The date from which this short URL will be valid. '
. 'If someone tries to access it before this date, it will not be found',
shortcut: 's',
)]
public string|null $validSince = null;
#[Option(
'The date until which this short URL will be valid. '
. 'If someone tries to access it after this date, it will not be found',
shortcut: 'u',
)]
public string|null $validUntil = null;
#[Option('This will limit the number of visits for this short URL', shortcut: 'm')]
public int|null $maxVisits = null;
#[Option('A descriptive title for the short URL')]
public string|null $title = null;
#[Option('Tells if this short URL will be included as "Allow" in Shlink\'s robots.txt', shortcut: 'r')]
public bool|null $crawlable = null;
#[Option(
'Disables the forwarding of the query string to the long URL, when the short URL is visited',
shortcut: 'w',
)]
public bool|null $noForwardQuery = null;
public function toArray(): array
{
$data = [];
// Avoid setting arguments that were not explicitly provided.
// This is important when editing short URLs and should not make a difference when creating.
if ($this->validSince !== null) {
$data[ShortUrlInputFilter::VALID_SINCE] = $this->validSince;
}
if ($this->validUntil !== null) {
$data[ShortUrlInputFilter::VALID_UNTIL] = $this->validUntil;
}
if ($this->maxVisits !== null) {
$data[ShortUrlInputFilter::MAX_VISITS] = $this->maxVisits;
}
if ($this->tags !== null) {
$data[ShortUrlInputFilter::TAGS] = array_unique($this->tags);
}
if ($this->title !== null) {
$data[ShortUrlInputFilter::TITLE] = $this->title;
}
if ($this->crawlable !== null) {
$data[ShortUrlInputFilter::CRAWLABLE] = $this->crawlable;
}
if ($this->noForwardQuery !== null) {
$data[ShortUrlInputFilter::FORWARD_QUERY] = !$this->noForwardQuery;
}
return $data;
}
}

View File

@@ -54,7 +54,7 @@ class CreateShortUrlCommandTest extends TestCase
);
$this->commandTester->execute([
'longUrl' => 'http://domain.com/foo/bar',
'long-url' => 'http://domain.com/foo/bar',
'--max-visits' => '3',
], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE]);
$output = $this->commandTester->getDisplay();
@@ -87,7 +87,7 @@ class CreateShortUrlCommandTest extends TestCase
);
$this->stringifier->method('stringify')->willReturn('');
$this->commandTester->execute(['longUrl' => 'http://domain.com/invalid', '--custom-slug' => 'my-slug']);
$this->commandTester->execute(['long-url' => 'http://domain.com/invalid', '--custom-slug' => 'my-slug']);
$output = $this->commandTester->getDisplay();
self::assertEquals(Command::FAILURE, $this->commandTester->getStatusCode());
@@ -109,7 +109,7 @@ class CreateShortUrlCommandTest extends TestCase
);
$this->commandTester->execute([
'longUrl' => 'http://domain.com/foo/bar',
'long-url' => 'http://domain.com/foo/bar',
'--tag' => ['foo', 'bar', 'baz', 'boo', 'zar', 'baz'],
]);
$output = $this->commandTester->getDisplay();
@@ -129,7 +129,7 @@ class CreateShortUrlCommandTest extends TestCase
)->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching(ShortUrl::createFake()));
$this->stringifier->method('stringify')->willReturn('');
$input['longUrl'] = 'http://domain.com/foo/bar';
$input['long-url'] = 'http://domain.com/foo/bar';
$this->commandTester->execute($input);
self::assertEquals(Command::SUCCESS, $this->commandTester->getStatusCode());
@@ -156,7 +156,7 @@ class CreateShortUrlCommandTest extends TestCase
)->willReturn(UrlShorteningResult::withoutErrorOnEventDispatching($shortUrl));
$this->stringifier->method('stringify')->willReturn('');
$options['longUrl'] = 'http://domain.com/foo/bar';
$options['long-url'] = 'http://domain.com/foo/bar';
$this->commandTester->execute($options);
}
@@ -178,7 +178,7 @@ class CreateShortUrlCommandTest extends TestCase
);
$this->stringifier->method('stringify')->willReturn('stringified_short_url');
$this->commandTester->execute(['longUrl' => 'http://domain.com/foo/bar'], ['verbosity' => $verbosity]);
$this->commandTester->execute(['long-url' => 'http://domain.com/foo/bar'], ['verbosity' => $verbosity]);
$output = $this->commandTester->getDisplay();
$assert($output);