mirror of
https://github.com/shlinkio/shlink.git
synced 2026-02-28 04:03:12 +08:00
Merge pull request #2546 from acelaya-forks/symfony-cli-improvements
Symfony cli improvements
This commit is contained in:
@@ -50,9 +50,9 @@
|
||||
"shlinkio/shlink-json": "dev-main#7c096d6 as 1.3.0",
|
||||
"spiral/roadrunner": "^2025.1",
|
||||
"spiral/roadrunner-cli": "^2.7",
|
||||
"spiral/roadrunner-http": "^3.5",
|
||||
"spiral/roadrunner-jobs": "^4.6",
|
||||
"symfony/console": "^8.0 || ^7.4",
|
||||
"spiral/roadrunner-http": "^3.6",
|
||||
"spiral/roadrunner-jobs": "^4.7",
|
||||
"symfony/console": "^8.0",
|
||||
"symfony/filesystem": "^8.0",
|
||||
"symfony/lock": "^8.0",
|
||||
"symfony/process": "^8.0",
|
||||
|
||||
@@ -9,9 +9,9 @@ use Shlinkio\Shlink\Core\Domain\DomainServiceInterface;
|
||||
use Shlinkio\Shlink\Core\Domain\Model\DomainItem;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Attribute\Interact;
|
||||
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 array_filter;
|
||||
@@ -32,7 +32,8 @@ class DomainRedirectsCommand extends Command
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
#[Interact]
|
||||
public function askDomain(InputInterface $input, SymfonyStyle $io): void
|
||||
{
|
||||
/** @var string|null $domain */
|
||||
$domain = $input->getArgument('domain');
|
||||
@@ -40,7 +41,6 @@ class DomainRedirectsCommand extends Command
|
||||
return;
|
||||
}
|
||||
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$askNewDomain = static fn () => $io->ask('Domain authority for which you want to set specific redirects');
|
||||
|
||||
/** @var string[] $availableDomains */
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\Integration;
|
||||
|
||||
use Cake\Chronos\Chronos;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoOptions;
|
||||
use Shlinkio\Shlink\Core\Matomo\MatomoVisitSenderInterface;
|
||||
use Shlinkio\Shlink\Core\Matomo\VisitSendingProgressTrackerInterface;
|
||||
@@ -17,10 +16,12 @@ use Throwable;
|
||||
|
||||
use function Shlinkio\Shlink\Common\buildDateRange;
|
||||
use function Shlinkio\Shlink\Core\dateRangeToHumanFriendly;
|
||||
use function Shlinkio\Shlink\Core\normalizeOptionalDate;
|
||||
use function sprintf;
|
||||
|
||||
#[AsCommand(
|
||||
name: MatomoSendVisitsCommand::NAME,
|
||||
description: 'Send existing visits to the configured matomo instance',
|
||||
help: <<<HELP
|
||||
This command allows you to send existing visits from this Shlink instance to the configured Matomo server.
|
||||
|
||||
@@ -56,14 +57,6 @@ class MatomoSendVisitsCommand extends Command implements VisitSendingProgressTra
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setDescription(sprintf(
|
||||
'%sSend existing visits to the configured matomo instance',
|
||||
$this->matomoEnabled ? '' : '<comment>[MATOMO INTEGRATION DISABLED]</comment> ',
|
||||
));
|
||||
}
|
||||
|
||||
public function __invoke(
|
||||
SymfonyStyle $io,
|
||||
InputInterface $input,
|
||||
@@ -81,8 +74,8 @@ class MatomoSendVisitsCommand extends Command implements VisitSendingProgressTra
|
||||
|
||||
// TODO Validate provided date formats
|
||||
$dateRange = buildDateRange(
|
||||
startDate: $since !== null ? Chronos::parse($since) : null,
|
||||
endDate: $until !== null ? Chronos::parse($until) : null,
|
||||
startDate: normalizeOptionalDate($since),
|
||||
endDate: normalizeOptionalDate($until),
|
||||
);
|
||||
|
||||
if ($input->isInteractive()) {
|
||||
|
||||
@@ -4,48 +4,41 @@ declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Command\RedirectRule;
|
||||
|
||||
use Shlinkio\Shlink\CLI\Input\ShortUrlIdentifierInput;
|
||||
use Shlinkio\Shlink\CLI\RedirectRule\RedirectRuleHandlerInterface;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\RedirectRule\ShortUrlRedirectRuleServiceInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlResolverInterface;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
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;
|
||||
|
||||
#[AsCommand(
|
||||
name: ManageRedirectRulesCommand::NAME,
|
||||
description: 'Set redirect rules for a short URL',
|
||||
)]
|
||||
class ManageRedirectRulesCommand extends Command
|
||||
{
|
||||
public const string NAME = 'short-url:manage-rules';
|
||||
|
||||
private readonly ShortUrlIdentifierInput $shortUrlIdentifierInput;
|
||||
|
||||
public function __construct(
|
||||
protected readonly ShortUrlResolverInterface $shortUrlResolver,
|
||||
protected readonly ShortUrlRedirectRuleServiceInterface $ruleService,
|
||||
protected readonly RedirectRuleHandlerInterface $ruleHandler,
|
||||
) {
|
||||
parent::__construct();
|
||||
$this->shortUrlIdentifierInput = new ShortUrlIdentifierInput(
|
||||
$this,
|
||||
shortCodeDesc: 'The short code which rules we want to set.',
|
||||
domainDesc: 'The domain for the short code.',
|
||||
);
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName(self::NAME)
|
||||
->setDescription('Set redirect rules for a short URL');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$identifier = $this->shortUrlIdentifierInput->toShortUrlIdentifier($input);
|
||||
public function __invoke(
|
||||
SymfonyStyle $io,
|
||||
#[Argument('The short code which rules we want to set')] string $shortCode,
|
||||
#[Option('The domain of the short code', shortcut: 'd')] string|null $domain = null,
|
||||
): int {
|
||||
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, $domain);
|
||||
|
||||
try {
|
||||
$shortUrl = $this->shortUrlResolver->resolveShortUrl($identifier);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,55 +4,49 @@ 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\Command\ShortUrl\Input\ShortUrlDataInput;
|
||||
use Shlinkio\Shlink\Core\Exception\ShortUrlNotFoundException;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Helper\ShortUrlStringifierInterface;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlIdentifier;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlServiceInterface;
|
||||
use Symfony\Component\Console\Attribute\Argument;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Attribute\MapInput;
|
||||
use Symfony\Component\Console\Attribute\Option;
|
||||
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;
|
||||
|
||||
#[AsCommand(
|
||||
name: EditShortUrlCommand::NAME,
|
||||
description: 'Edit an existing short URL',
|
||||
)]
|
||||
class EditShortUrlCommand extends Command
|
||||
{
|
||||
public const string 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);
|
||||
public function __invoke(
|
||||
SymfonyStyle $io,
|
||||
#[Argument('The short code to edit')] string $shortCode,
|
||||
#[MapInput] ShortUrlDataInput $data,
|
||||
#[Option('The domain to which the short URL is attached', shortcut: 'd')] string|null $domain = null,
|
||||
#[Option('The long URL to set', shortcut: 'l')] string|null $longUrl = null,
|
||||
): int {
|
||||
$identifier = ShortUrlIdentifier::fromShortCodeAndDomain($shortCode, $domain);
|
||||
|
||||
try {
|
||||
$shortUrl = $this->shortUrlService->updateShortUrl(
|
||||
$identifier,
|
||||
$this->shortUrlDataInput->toShortUrlEdition($input),
|
||||
ShortUrlEdition::fromRawData($data->toArray()),
|
||||
);
|
||||
|
||||
$io->success(sprintf('Short URL "%s" properly edited', $this->stringifier->stringify($shortUrl)));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
80
module/CLI/src/Command/ShortUrl/Input/ShortUrlDataInput.php
Normal file
80
module/CLI/src/Command/ShortUrl/Input/ShortUrlDataInput.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -45,16 +45,16 @@ final class ShortUrlsParamsInput
|
||||
)]
|
||||
public string|null $domain = null;
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[]|null */
|
||||
#[Option('A list of tags that short URLs need to include', name: 'tag', shortcut: 't')]
|
||||
public array $tags = [];
|
||||
public array|null $tags = null;
|
||||
|
||||
#[Option('If --tag is provided, returns only short URLs including ALL of them')]
|
||||
public bool $tagsAll = false;
|
||||
|
||||
/** @var string[] */
|
||||
/** @var string[]|null */
|
||||
#[Option('A list of tags that short URLs should NOT include', name: 'exclude-tag', shortcut: 'et')]
|
||||
public array $excludeTags = [];
|
||||
public array|null $excludeTags = null;
|
||||
|
||||
#[Option('If --exclude-tag is provided, returns only short URLs not including ANY of them')]
|
||||
public bool $excludeTagsAll = false;
|
||||
@@ -88,17 +88,10 @@ final class ShortUrlsParamsInput
|
||||
|
||||
public function toArray(OutputInterface $output): array
|
||||
{
|
||||
$tagsMode = $this->tagsAll ? TagsMode::ALL->value : TagsMode::ANY->value;
|
||||
$excludeTagsMode = $this->excludeTagsAll ? TagsMode::ALL->value : TagsMode::ANY->value;
|
||||
|
||||
$data = [
|
||||
ShortUrlsParamsInputFilter::PAGE => $this->page,
|
||||
ShortUrlsParamsInputFilter::SEARCH_TERM => $this->searchTerm,
|
||||
ShortUrlsParamsInputFilter::DOMAIN => $this->domain,
|
||||
ShortUrlsParamsInputFilter::TAGS => array_unique($this->tags),
|
||||
ShortUrlsParamsInputFilter::TAGS_MODE => $tagsMode,
|
||||
ShortUrlsParamsInputFilter::EXCLUDE_TAGS => array_unique($this->excludeTags),
|
||||
ShortUrlsParamsInputFilter::EXCLUDE_TAGS_MODE => $excludeTagsMode,
|
||||
ShortUrlsParamsInputFilter::ORDER_BY => $this->orderBy,
|
||||
ShortUrlsParamsInputFilter::START_DATE => InputUtils::processDate('start-date', $this->startDate, $output),
|
||||
ShortUrlsParamsInputFilter::END_DATE => InputUtils::processDate('end-date', $this->endDate, $output),
|
||||
@@ -107,6 +100,18 @@ final class ShortUrlsParamsInput
|
||||
ShortUrlsParamsInputFilter::API_KEY_NAME => $this->apiKeyName,
|
||||
];
|
||||
|
||||
if ($this->tags !== null) {
|
||||
$tagsMode = $this->tagsAll ? TagsMode::ALL : TagsMode::ANY;
|
||||
$data[ShortUrlsParamsInputFilter::TAGS_MODE] = $tagsMode->value;
|
||||
$data[ShortUrlsParamsInputFilter::TAGS] = array_unique($this->tags);
|
||||
}
|
||||
|
||||
if ($this->excludeTags !== null) {
|
||||
$excludeTagsMode = $this->excludeTagsAll ? TagsMode::ALL : TagsMode::ANY;
|
||||
$data[ShortUrlsParamsInputFilter::EXCLUDE_TAGS_MODE] = $excludeTagsMode->value;
|
||||
$data[ShortUrlsParamsInputFilter::EXCLUDE_TAGS] = array_unique($this->excludeTags);
|
||||
}
|
||||
|
||||
if ($this->all) {
|
||||
$data[ShortUrlsParamsInputFilter::ITEMS_PER_PAGE] = Paginator::ALL_ITEMS;
|
||||
}
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Input;
|
||||
|
||||
use Shlinkio\Shlink\Core\Config\Options\UrlShortenerOptions;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlCreation;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlEdition;
|
||||
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlInputFilter;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
final readonly class ShortUrlDataInput
|
||||
{
|
||||
private readonly TagsOption $tagsOption;
|
||||
|
||||
public function __construct(Command $command, private bool $longUrlAsOption = false)
|
||||
{
|
||||
if ($longUrlAsOption) {
|
||||
$command->addOption('long-url', 'l', InputOption::VALUE_REQUIRED, 'The long URL to set');
|
||||
} else {
|
||||
$command->addArgument('longUrl', InputArgument::REQUIRED, 'The long URL to set');
|
||||
}
|
||||
|
||||
$this->tagsOption = new TagsOption($command, 'Tags to apply to the short URL');
|
||||
|
||||
$command
|
||||
->addOption(
|
||||
ShortUrlDataOption::VALID_SINCE->value,
|
||||
ShortUrlDataOption::VALID_SINCE->shortcut(),
|
||||
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(
|
||||
ShortUrlDataOption::VALID_UNTIL->value,
|
||||
ShortUrlDataOption::VALID_UNTIL->shortcut(),
|
||||
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.',
|
||||
)
|
||||
->addOption(
|
||||
ShortUrlDataOption::MAX_VISITS->value,
|
||||
ShortUrlDataOption::MAX_VISITS->shortcut(),
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'This will limit the number of visits for this short URL.',
|
||||
)
|
||||
->addOption(
|
||||
ShortUrlDataOption::TITLE->value,
|
||||
ShortUrlDataOption::TITLE->shortcut(),
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'A descriptive title for the short URL.',
|
||||
)
|
||||
->addOption(
|
||||
ShortUrlDataOption::CRAWLABLE->value,
|
||||
ShortUrlDataOption::CRAWLABLE->shortcut(),
|
||||
InputOption::VALUE_NONE,
|
||||
'Tells if this short URL will be included as "Allow" in Shlink\'s robots.txt.',
|
||||
)
|
||||
->addOption(
|
||||
ShortUrlDataOption::NO_FORWARD_QUERY->value,
|
||||
ShortUrlDataOption::NO_FORWARD_QUERY->shortcut(),
|
||||
InputOption::VALUE_NONE,
|
||||
'Disables the forwarding of the query string to the long URL, when the short URL is visited.',
|
||||
);
|
||||
}
|
||||
|
||||
public function toShortUrlEdition(InputInterface $input): ShortUrlEdition
|
||||
{
|
||||
return ShortUrlEdition::fromRawData($this->getCommonData($input));
|
||||
}
|
||||
|
||||
public function toShortUrlCreation(
|
||||
InputInterface $input,
|
||||
UrlShortenerOptions $options,
|
||||
string $customSlugField,
|
||||
string $shortCodeLengthField,
|
||||
string $pathPrefixField,
|
||||
string $findIfExistsField,
|
||||
string $domainField,
|
||||
): ShortUrlCreation {
|
||||
$shortCodeLength = $input->getOption($shortCodeLengthField) ?? $options->defaultShortCodesLength;
|
||||
return ShortUrlCreation::fromRawData([
|
||||
...$this->getCommonData($input),
|
||||
ShortUrlInputFilter::CUSTOM_SLUG => $input->getOption($customSlugField),
|
||||
ShortUrlInputFilter::SHORT_CODE_LENGTH => $shortCodeLength,
|
||||
ShortUrlInputFilter::PATH_PREFIX => $input->getOption($pathPrefixField),
|
||||
ShortUrlInputFilter::FIND_IF_EXISTS => $input->getOption($findIfExistsField),
|
||||
ShortUrlInputFilter::DOMAIN => $input->getOption($domainField),
|
||||
], $options);
|
||||
}
|
||||
|
||||
private function getCommonData(InputInterface $input): array
|
||||
{
|
||||
$longUrl = $this->longUrlAsOption ? $input->getOption('long-url') : $input->getArgument('longUrl');
|
||||
$data = [ShortUrlInputFilter::LONG_URL => $longUrl];
|
||||
|
||||
// Avoid setting arguments that were not explicitly provided.
|
||||
// This is important when editing short URLs and should not make a difference when creating.
|
||||
if (ShortUrlDataOption::VALID_SINCE->wasProvided($input)) {
|
||||
$data[ShortUrlInputFilter::VALID_SINCE] = $input->getOption('valid-since');
|
||||
}
|
||||
if (ShortUrlDataOption::VALID_UNTIL->wasProvided($input)) {
|
||||
$data[ShortUrlInputFilter::VALID_UNTIL] = $input->getOption('valid-until');
|
||||
}
|
||||
if (ShortUrlDataOption::MAX_VISITS->wasProvided($input)) {
|
||||
$maxVisits = $input->getOption('max-visits');
|
||||
$data[ShortUrlInputFilter::MAX_VISITS] = $maxVisits !== null ? (int) $maxVisits : null;
|
||||
}
|
||||
if ($this->tagsOption->exists($input)) {
|
||||
$data[ShortUrlInputFilter::TAGS] = $this->tagsOption->get($input);
|
||||
}
|
||||
if (ShortUrlDataOption::TITLE->wasProvided($input)) {
|
||||
$data[ShortUrlInputFilter::TITLE] = $input->getOption('title');
|
||||
}
|
||||
if (ShortUrlDataOption::CRAWLABLE->wasProvided($input)) {
|
||||
$data[ShortUrlInputFilter::CRAWLABLE] = $input->getOption('crawlable');
|
||||
}
|
||||
if (ShortUrlDataOption::NO_FORWARD_QUERY->wasProvided($input)) {
|
||||
$data[ShortUrlInputFilter::FORWARD_QUERY] = !$input->getOption('no-forward-query');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Input;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
enum ShortUrlDataOption: string
|
||||
{
|
||||
case VALID_SINCE = 'valid-since';
|
||||
case VALID_UNTIL = 'valid-until';
|
||||
case MAX_VISITS = 'max-visits';
|
||||
case TITLE = 'title';
|
||||
case CRAWLABLE = 'crawlable';
|
||||
case NO_FORWARD_QUERY = 'no-forward-query';
|
||||
|
||||
public function shortcut(): string|null
|
||||
{
|
||||
return match ($this) {
|
||||
self::VALID_SINCE => 's',
|
||||
self::VALID_UNTIL => 'u',
|
||||
self::MAX_VISITS => 'm',
|
||||
self::TITLE => null,
|
||||
self::CRAWLABLE => 'r',
|
||||
self::NO_FORWARD_QUERY => 'w',
|
||||
};
|
||||
}
|
||||
|
||||
public function wasProvided(InputInterface $input): bool
|
||||
{
|
||||
$option = sprintf('--%s', $this->value);
|
||||
$shortcut = $this->shortcut();
|
||||
|
||||
return $input->hasParameterOption($shortcut === null ? $option : [$option, sprintf('-%s', $shortcut)]);
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Shlinkio\Shlink\CLI\Input;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
|
||||
use function array_unique;
|
||||
|
||||
readonly class TagsOption
|
||||
{
|
||||
public function __construct(Command $command, string $description)
|
||||
{
|
||||
$command
|
||||
->addOption(
|
||||
'tag',
|
||||
't',
|
||||
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
|
||||
$description,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether tags have been set or not, via `--tag` or `-t`
|
||||
*/
|
||||
public function exists(InputInterface $input): bool
|
||||
{
|
||||
return $input->hasParameterOption(['--tag', '-t']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function get(InputInterface $input): array
|
||||
{
|
||||
return array_unique($input->getOption('tag'));
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,7 @@ class ManageRedirectRulesCommandTest extends TestCase
|
||||
$this->ruleService->expects($this->never())->method('saveRulesForShortUrl');
|
||||
$this->ruleHandler->expects($this->never())->method('manageRules');
|
||||
|
||||
$exitCode = $this->commandTester->execute(['shortCode' => 'foo']);
|
||||
$exitCode = $this->commandTester->execute(['short-code' => 'foo']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals(Command::FAILURE, $exitCode);
|
||||
@@ -67,7 +67,7 @@ class ManageRedirectRulesCommandTest extends TestCase
|
||||
$this->ruleHandler->expects($this->once())->method('manageRules')->willReturn(null);
|
||||
$this->ruleService->expects($this->never())->method('saveRulesForShortUrl');
|
||||
|
||||
$exitCode = $this->commandTester->execute(['shortCode' => 'foo']);
|
||||
$exitCode = $this->commandTester->execute(['short-code' => 'foo']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals(Command::SUCCESS, $exitCode);
|
||||
@@ -86,7 +86,7 @@ class ManageRedirectRulesCommandTest extends TestCase
|
||||
$this->ruleHandler->expects($this->once())->method('manageRules')->willReturn([]);
|
||||
$this->ruleService->expects($this->once())->method('saveRulesForShortUrl')->with($shortUrl, []);
|
||||
|
||||
$exitCode = $this->commandTester->execute(['shortCode' => 'foo']);
|
||||
$exitCode = $this->commandTester->execute(['short-code' => 'foo']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
|
||||
self::assertEquals(Command::SUCCESS, $exitCode);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -40,7 +40,7 @@ class EditShortUrlCommandTest extends TestCase
|
||||
);
|
||||
$this->stringifier->expects($this->once())->method('stringify')->willReturn('https://s.test/foo');
|
||||
|
||||
$this->commandTester->execute(['shortCode' => 'foobar']);
|
||||
$this->commandTester->execute(['short-code' => 'foobar']);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$exitCode = $this->commandTester->getStatusCode();
|
||||
|
||||
@@ -59,7 +59,7 @@ class EditShortUrlCommandTest extends TestCase
|
||||
$this->shortUrlService->expects($this->once())->method('updateShortUrl')->willThrowException($e);
|
||||
$this->stringifier->expects($this->never())->method('stringify');
|
||||
|
||||
$this->commandTester->execute(['shortCode' => 'foo'], ['verbosity' => $verbosity]);
|
||||
$this->commandTester->execute(['short-code' => 'foo'], ['verbosity' => $verbosity]);
|
||||
$output = $this->commandTester->getDisplay();
|
||||
$exitCode = $this->commandTester->getStatusCode();
|
||||
|
||||
|
||||
@@ -203,25 +203,34 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
array $commandArgs,
|
||||
int|null $page,
|
||||
string|null $searchTerm,
|
||||
array $tags,
|
||||
array|null $tags,
|
||||
string $tagsMode,
|
||||
string|null $startDate = null,
|
||||
string|null $endDate = null,
|
||||
array $excludeTags = [],
|
||||
array|null $excludeTags = null,
|
||||
string $excludeTagsMode = TagsMode::ANY->value,
|
||||
string|null $apiKeyName = null,
|
||||
): void {
|
||||
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(ShortUrlsParams::fromRawData([
|
||||
$expectedData = [
|
||||
'page' => $page,
|
||||
'searchTerm' => $searchTerm,
|
||||
'tags' => $tags,
|
||||
'tagsMode' => $tagsMode,
|
||||
'startDate' => $startDate !== null ? Chronos::parse($startDate)->toAtomString() : null,
|
||||
'endDate' => $endDate !== null ? Chronos::parse($endDate)->toAtomString() : null,
|
||||
'excludeTags' => $excludeTags,
|
||||
'excludeTagsMode' => $excludeTagsMode,
|
||||
'apiKeyName' => $apiKeyName,
|
||||
]))->willReturn(new Paginator(new ArrayAdapter([])));
|
||||
];
|
||||
|
||||
if ($tags !== null) {
|
||||
$expectedData['tags'] = $tags;
|
||||
}
|
||||
if ($excludeTags !== null) {
|
||||
$expectedData['excludeTags'] = $excludeTags;
|
||||
}
|
||||
|
||||
$this->shortUrlService->expects($this->once())->method('listShortUrls')->with(ShortUrlsParams::fromRawData(
|
||||
$expectedData,
|
||||
))->willReturn(new Paginator(new ArrayAdapter([])));
|
||||
|
||||
$this->commandTester->setInputs(['n']);
|
||||
$this->commandTester->execute($commandArgs);
|
||||
@@ -231,7 +240,7 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
{
|
||||
yield [[], 1, null, [], TagsMode::ANY->value];
|
||||
yield [['--page' => $page = 3], $page, null, [], TagsMode::ANY->value];
|
||||
yield [['--tags-all' => true], 1, null, [], TagsMode::ALL->value];
|
||||
yield [['--tags-all' => true, '--tag' => ['foo']], 1, null, ['foo'], TagsMode::ALL->value];
|
||||
yield [['--search-term' => $searchTerm = 'search this'], 1, $searchTerm, [], TagsMode::ANY->value];
|
||||
yield [
|
||||
['--page' => $page = 3, '--search-term' => $searchTerm = 'search this', '--tag' => $tags = ['foo', 'bar']],
|
||||
@@ -270,7 +279,7 @@ class ListShortUrlsCommandTest extends TestCase
|
||||
['--exclude-tag' => ['foo', 'bar'], '--exclude-tags-all' => true],
|
||||
1,
|
||||
null,
|
||||
[],
|
||||
null,
|
||||
TagsMode::ANY->value,
|
||||
null,
|
||||
null,
|
||||
|
||||
Reference in New Issue
Block a user