Skip to content

Commit

Permalink
Merge pull request #2164 from acelaya-forks/feature/update-url-cli
Browse files Browse the repository at this point in the history
Add command to update short URLs
  • Loading branch information
acelaya authored Jul 26, 2024
2 parents 38d8086 + a1afc90 commit 7f9dc10
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 84 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-db-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ jobs:
extensions-cache-key: db-tests-extensions-${{ matrix.php-version }}-${{ inputs.platform }}
- name: Create test database
if: ${{ inputs.platform == 'ms' }}
run: docker compose exec -T shlink_db_ms /opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
run: docker compose exec -T shlink_db_ms /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P 'Passw0rd!' -Q "CREATE DATABASE shlink_test;"
- name: Run tests
run: composer test:db:${{ inputs.platform }}
- name: Upload code coverage
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com), and this

* [#2018](https://github.com/shlinkio/shlink/issues/2018) Add option to allow all short URLs to be unconditionally crawlable in robots.txt, via `ROBOTS_ALLOW_ALL_SHORT_URLS=true` env var, or config option.
* [#2109](https://github.com/shlinkio/shlink/issues/2109) Add option to customize user agents robots.txt, via `ROBOTS_USER_AGENTS=foo,bar,baz` env var, or config option.
* [#2163](https://github.com/shlinkio/shlink/issues/2163) Add `short-urls:edit` command to edit existing short URLs.

This brings CLI and API interfaces capabilities closer, and solves an overlook since the feature was implemented years ago.

### Changed
* [#2096](https://github.com/shlinkio/shlink/issues/2096) Update to RoadRunner 2024.
Expand Down
1 change: 1 addition & 0 deletions module/CLI/config/cli.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
'cli' => [
'commands' => [
Command\ShortUrl\CreateShortUrlCommand::NAME => Command\ShortUrl\CreateShortUrlCommand::class,
Command\ShortUrl\EditShortUrlCommand::NAME => Command\ShortUrl\EditShortUrlCommand::class,
Command\ShortUrl\ResolveUrlCommand::NAME => Command\ShortUrl\ResolveUrlCommand::class,
Command\ShortUrl\ListShortUrlsCommand::NAME => Command\ShortUrl\ListShortUrlsCommand::class,
Command\ShortUrl\GetShortUrlVisitsCommand::NAME => Command\ShortUrl\GetShortUrlVisitsCommand::class,
Expand Down
2 changes: 2 additions & 0 deletions module/CLI/config/dependencies.config.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
ApiKey\RoleResolver::class => ConfigAbstractFactory::class,

Command\ShortUrl\CreateShortUrlCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\EditShortUrlCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\ResolveUrlCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\ListShortUrlsCommand::class => ConfigAbstractFactory::class,
Command\ShortUrl\GetShortUrlVisitsCommand::class => ConfigAbstractFactory::class,
Expand Down Expand Up @@ -92,6 +93,7 @@
ShortUrlStringifier::class,
UrlShortenerOptions::class,
],
Command\ShortUrl\EditShortUrlCommand::class => [ShortUrl\ShortUrlService::class, ShortUrlStringifier::class],
Command\ShortUrl\ResolveUrlCommand::class => [ShortUrl\ShortUrlResolver::class],
Command\ShortUrl\ListShortUrlsCommand::class => [
ShortUrl\ShortUrlListService::class,
Expand Down
104 changes: 23 additions & 81 deletions module/CLI/src/Command/ShortUrl/CreateShortUrlCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,65 +4,46 @@

namespace Shlinkio\Shlink\CLI\Command\ShortUrl;

use Shlinkio\Shlink\CLI\Input\ShortUrlDataInput;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\NonUniqueSlugException;
use Shlinkio\Shlink\Core\Options\UrlShortenerOptions;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
use Shlinkio\Shlink\Core\ShortUrl\UrlShortenerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
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 array_map;
use function array_unique;
use function explode;
use function Shlinkio\Shlink\Core\ArrayUtils\flatten;
use function sprintf;

class CreateShortUrlCommand extends Command
{
public const 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
{
$this
->setName(self::NAME)
->setDescription('Generates a short URL for provided long URL and returns it')
->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to parse')
->addOption(
'tags',
't',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
'Tags to apply to the new short URL',
)
->addOption(
'valid-since',
's',
InputOption::VALUE_REQUIRED,
'The date from which this short URL will be valid. '
. 'If someone tries to access it before this date, it will not be found.',
)
->addOption(
'valid-until',
'u',
'domain',
'd',
InputOption::VALUE_REQUIRED,
'The date until which this short URL will be valid. '
. 'If someone tries to access it after this date, it will not be found.',
'The domain to which this short URL will be attached.',
)
->addOption(
'custom-slug',
Expand All @@ -71,46 +52,22 @@ protected function configure(): void
'If provided, this slug will be used instead of generating a short code',
)
->addOption(
'path-prefix',
'p',
'short-code-length',
'l',
InputOption::VALUE_REQUIRED,
'Prefix to prepend before the generated short code or provided custom slug',
'The length for generated short code (it will be ignored if --custom-slug was provided).',
)
->addOption(
'max-visits',
'm',
'path-prefix',
'p',
InputOption::VALUE_REQUIRED,
'This will limit the number of visits for this short URL.',
'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.',
)
->addOption(
'domain',
'd',
InputOption::VALUE_REQUIRED,
'The domain to which this short URL will be attached.',
)
->addOption(
'short-code-length',
'l',
InputOption::VALUE_REQUIRED,
'The length for generated short code (it will be ignored if --custom-slug was provided).',
)
->addOption(
'crawlable',
'r',
InputOption::VALUE_NONE,
'Tells if this URL will be included as "Allow" in Shlink\'s robots.txt.',
)
->addOption(
'no-forward-query',
'w',
InputOption::VALUE_NONE,
'Disables the forwarding of the query string to the long URL, when the new short URL is visited.',
);
}

Expand All @@ -136,40 +93,25 @@ private function verifyLongUrlArgument(InputInterface $input, OutputInterface $o
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = $this->getIO($input, $output);
$longUrl = $input->getArgument('longUrl');
if (empty($longUrl)) {
$io->error('A URL was not provided!');
return ExitCode::EXIT_FAILURE;
}

$explodeWithComma = static fn (string $tag) => explode(',', $tag);
$tags = array_unique(flatten(array_map($explodeWithComma, $input->getOption('tags'))));
$maxVisits = $input->getOption('max-visits');
$shortCodeLength = $input->getOption('short-code-length') ?? $this->options->defaultShortCodesLength;

try {
$result = $this->urlShortener->shorten(ShortUrlCreation::fromRawData([
ShortUrlInputFilter::LONG_URL => $longUrl,
ShortUrlInputFilter::VALID_SINCE => $input->getOption('valid-since'),
ShortUrlInputFilter::VALID_UNTIL => $input->getOption('valid-until'),
ShortUrlInputFilter::MAX_VISITS => $maxVisits !== null ? (int) $maxVisits : null,
ShortUrlInputFilter::CUSTOM_SLUG => $input->getOption('custom-slug'),
ShortUrlInputFilter::PATH_PREFIX => $input->getOption('path-prefix'),
ShortUrlInputFilter::FIND_IF_EXISTS => $input->getOption('find-if-exists'),
ShortUrlInputFilter::DOMAIN => $input->getOption('domain'),
ShortUrlInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
ShortUrlInputFilter::TAGS => $tags,
ShortUrlInputFilter::CRAWLABLE => $input->getOption('crawlable'),
ShortUrlInputFilter::FORWARD_QUERY => !$input->getOption('no-forward-query'),
], $this->options));
$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->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.',
));

$io->writeln([
sprintf('Processed long URL: <info>%s</info>', $longUrl),
sprintf('Processed long URL: <info>%s</info>', $result->shortUrl->getLongUrl()),
sprintf('Generated short URL: <info>%s</info>', $this->stringifier->stringify($result->shortUrl)),
]);
return ExitCode::EXIT_SUCCESS;
Expand All @@ -181,6 +123,6 @@ protected function execute(InputInterface $input, OutputInterface $output): int

private function getIO(InputInterface $input, OutputInterface $output): SymfonyStyle
{
return $this->io ?? ($this->io = new SymfonyStyle($input, $output));
return $this->io ??= new SymfonyStyle($input, $output);
}
}
71 changes: 71 additions & 0 deletions module/CLI/src/Command/ShortUrl/EditShortUrlCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace Shlinkio\Shlink\CLI\Command\ShortUrl;

use Shlinkio\Shlink\CLI\Input\ShortUrlDataInput;
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
use Shlinkio\Shlink\CLI\Util\ExitCode;
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

use function sprintf;

class EditShortUrlCommand extends Command
{
public const NAME = 'short-url:edit';

private readonly ShortUrlDataInput $shortUrlDataInput;
private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput;

public function __construct(
private readonly ShortUrlServiceInterface $shortUrlService,
private readonly ShortUrlStringifierInterface $stringifier,
) {
parent::__construct();

$this->shortUrlDataInput = new ShortUrlDataInput($this, longUrlAsOption: true);
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
$this,
shortCodeDesc: 'The short code to edit',
domainDesc: 'The domain to which the short URL is attached.',
);
}

protected function configure(): void
{
$this
->setName(self::NAME)
->setDescription('Edit an existing short URL');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);

try {
$shortUrl = $this->shortUrlService->updateShortUrl(
$identifier,
$this->shortUrlDataInput->toShortUrlEdition($input),
);

$io->success(sprintf('Short URL "%s" properly edited', $this->stringifier->stringify($shortUrl)));
return ExitCode::EXIT_SUCCESS;
} catch (ShortUrlNotFoundException $e) {
$io->error(sprintf('Short URL not found for "%s"', $identifier->__toString()));

if ($io->isVerbose()) {
$this->getApplication()?->renderThrowable($e, $io);
}

return ExitCode::EXIT_FAILURE;
}
}
}
Loading

0 comments on commit 7f9dc10

Please sign in to comment.