Merge pull request #1565 from acelaya-forks/feature/command-reusable-args

Feature/command reusable args
This commit is contained in:
Alejandro Celaya 2022-10-06 21:38:19 +02:00 committed by GitHub
commit 46f948a584
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 226 additions and 110 deletions

View file

@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com), and this project adheres to [Semantic Versioning](https://semver.org).
## [Unreleased]
### Added
* *Nothing*
### Changed
* [#1563](https://github.com/shlinkio/shlink/issues/1563) Moved logic to reuse command options to option classes instead of base abstract command classes.
### Deprecated
* *Nothing*
### Removed
* *Nothing*
### Fixed
* *Nothing*
## [3.3.1] - 2022-09-30
### Added
* *Nothing*

View file

@ -45,8 +45,8 @@
"php-middleware/request-id": "^4.1",
"pugx/shortid-php": "^1.0",
"ramsey/uuid": "^4.3",
"shlinkio/shlink-common": "^5.1",
"shlinkio/shlink-config": "^2.1",
"shlinkio/shlink-common": "dev-main#7515008 as 5.2",
"shlinkio/shlink-config": "dev-main#bcd8222 as 2.2",
"shlinkio/shlink-event-dispatcher": "^2.6",
"shlinkio/shlink-importer": "^4.0",
"shlinkio/shlink-installer": "^8.2",

View file

@ -25,7 +25,7 @@ class GetDomainVisitsCommand extends AbstractVisitsListCommand
parent::__construct($visitsHelper);
}
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)

View file

@ -20,7 +20,7 @@ class GetShortUrlVisitsCommand extends AbstractVisitsListCommand
{
public const NAME = 'short-url:visits';
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)

View file

@ -4,7 +4,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
use Shlinkio\Shlink\CLI\Option\EndDateOption;
use Shlinkio\Shlink\CLI\Option\StartDateOption;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Paginator;
@ -15,6 +16,7 @@ use Shlinkio\Shlink\Core\ShortUrl\Model\ShortUrlsParams;
use Shlinkio\Shlink\Core\ShortUrl\Model\TagsMode;
use Shlinkio\Shlink\Core\ShortUrl\Model\Validation\ShortUrlsParamsInputFilter;
use Shlinkio\Shlink\Core\ShortUrl\ShortUrlServiceInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
@ -27,20 +29,25 @@ use function Functional\map;
use function implode;
use function sprintf;
class ListShortUrlsCommand extends AbstractWithDateRangeCommand
class ListShortUrlsCommand extends Command
{
use PagerfantaUtilsTrait;
public const NAME = 'short-url:list';
private readonly StartDateOption $startDateOption;
private readonly EndDateOption $endDateOption;
public function __construct(
private ShortUrlServiceInterface $shortUrlService,
private DataTransformerInterface $transformer,
private readonly ShortUrlServiceInterface $shortUrlService,
private readonly DataTransformerInterface $transformer,
) {
parent::__construct();
$this->startDateOption = new StartDateOption($this, 'short URLs');
$this->endDateOption = new EndDateOption($this, 'short URLs');
}
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)
@ -104,16 +111,6 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
);
}
protected function getStartDateDesc(string $optionName): string
{
return sprintf('Allows to filter short URLs, returning only those created after "%s".', $optionName);
}
protected function getEndDateDesc(string $optionName): string
{
return sprintf('Allows to filter short URLs, returning only those created before "%s".', $optionName);
}
protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$io = new SymfonyStyle($input, $output);
@ -124,8 +121,8 @@ class ListShortUrlsCommand extends AbstractWithDateRangeCommand
$tagsMode = $input->getOption('including-all-tags') === true ? TagsMode::ALL->value : TagsMode::ANY->value;
$tags = ! empty($tags) ? explode(',', $tags) : [];
$all = $input->getOption('all');
$startDate = $this->getStartDateOption($input, $output);
$endDate = $this->getEndDateOption($input, $output);
$startDate = $this->startDateOption->get($input, $output);
$endDate = $this->endDateOption->get($input, $output);
$orderBy = $this->processOrderBy($input);
$columnsMap = $this->resolveColumnsMap($input);

View file

@ -25,7 +25,7 @@ class GetTagVisitsCommand extends AbstractVisitsListCommand
parent::__construct($visitsHelper);
}
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)

View file

@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Util;
use Cake\Chronos\Chronos;
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 Throwable;
use function is_string;
use function sprintf;
abstract class AbstractWithDateRangeCommand extends Command
{
private const START_DATE = 'start-date';
private const END_DATE = 'end-date';
final protected function configure(): void
{
$this->doConfigure();
$this
->addOption(self::START_DATE, 's', InputOption::VALUE_REQUIRED, $this->getStartDateDesc(self::START_DATE))
->addOption(self::END_DATE, 'e', InputOption::VALUE_REQUIRED, $this->getEndDateDesc(self::END_DATE));
}
protected function getStartDateOption(InputInterface $input, OutputInterface $output): ?Chronos
{
return $this->getDateOption($input, $output, self::START_DATE);
}
protected function getEndDateOption(InputInterface $input, OutputInterface $output): ?Chronos
{
return $this->getDateOption($input, $output, self::END_DATE);
}
private function getDateOption(InputInterface $input, OutputInterface $output, string $key): ?Chronos
{
$value = $input->getOption($key);
if (empty($value) || ! is_string($value)) {
return null;
}
try {
return Chronos::parse($value);
} catch (Throwable $e) {
$output->writeln(sprintf(
'<comment>> Ignored provided "%s" since its value "%s" is not a valid date. <</comment>',
$key,
$value,
));
if ($output->isVeryVerbose()) {
$this->getApplication()?->renderThrowable($e, $output);
}
return null;
}
}
abstract protected function doConfigure(): void;
abstract protected function getStartDateDesc(string $optionName): string;
abstract protected function getEndDateDesc(string $optionName): string;
}

View file

@ -4,13 +4,15 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Visit;
use Shlinkio\Shlink\CLI\Command\Util\AbstractWithDateRangeCommand;
use Shlinkio\Shlink\CLI\Option\EndDateOption;
use Shlinkio\Shlink\CLI\Option\StartDateOption;
use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Paginator;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Visit\Entity\Visit;
use Shlinkio\Shlink\Core\Visit\VisitsStatsHelperInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
@ -19,29 +21,23 @@ use function Functional\map;
use function Functional\select_keys;
use function Shlinkio\Shlink\Common\buildDateRange;
use function Shlinkio\Shlink\Core\camelCaseToHumanFriendly;
use function sprintf;
abstract class AbstractVisitsListCommand extends AbstractWithDateRangeCommand
abstract class AbstractVisitsListCommand extends Command
{
private readonly StartDateOption $startDateOption;
private readonly EndDateOption $endDateOption;
public function __construct(protected readonly VisitsStatsHelperInterface $visitsHelper)
{
parent::__construct();
}
final protected function getStartDateDesc(string $optionName): string
{
return sprintf('Allows to filter visits, returning only those older than "%s".', $optionName);
}
final protected function getEndDateDesc(string $optionName): string
{
return sprintf('Allows to filter visits, returning only those newer than "%s".', $optionName);
$this->startDateOption = new StartDateOption($this, 'visits');
$this->endDateOption = new EndDateOption($this, 'visits');
}
final protected function execute(InputInterface $input, OutputInterface $output): ?int
{
$startDate = $this->getStartDateOption($input, $output);
$endDate = $this->getEndDateOption($input, $output);
$startDate = $this->startDateOption->get($input, $output);
$endDate = $this->endDateOption->get($input, $output);
$paginator = $this->getVisitsPaginator($input, buildDateRange($startDate, $endDate));
[$rows, $headers] = $this->resolveRowsAndHeaders($paginator);

View file

@ -23,7 +23,7 @@ class GetNonOrphanVisitsCommand extends AbstractVisitsListCommand
parent::__construct($visitsHelper);
}
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)

View file

@ -14,7 +14,7 @@ class GetOrphanVisitsCommand extends AbstractVisitsListCommand
{
public const NAME = 'visit:orphan';
protected function doConfigure(): void
protected function configure(): void
{
$this
->setName(self::NAME)

View file

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Option;
use Cake\Chronos\Chronos;
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 Throwable;
use function is_string;
use function sprintf;
class DateOption
{
public function __construct(
private readonly Command $command,
private readonly string $name,
string $shortcut,
string $description,
) {
$command->addOption($name, $shortcut, InputOption::VALUE_REQUIRED, $description);
}
public function get(InputInterface $input, OutputInterface $output): ?Chronos
{
$value = $input->getOption($this->name);
if (empty($value) || ! is_string($value)) {
return null;
}
try {
return Chronos::parse($value);
} catch (Throwable $e) {
$output->writeln(sprintf(
'<comment>> Ignored provided "%s" since its value "%s" is not a valid date. <</comment>',
$this->name,
$value,
));
if ($output->isVeryVerbose()) {
$this->command->getApplication()?->renderThrowable($e, $output);
}
return null;
}
}
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Option;
use Cake\Chronos\Chronos;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function sprintf;
class EndDateOption
{
private readonly DateOption $dateOption;
public function __construct(Command $command, string $descriptionHint)
{
$this->dateOption = new DateOption($command, 'end-date', 'e', sprintf(
'Allows to filter %s, returning only those newer than provided date.',
$descriptionHint,
));
}
public function get(InputInterface $input, OutputInterface $output): ?Chronos
{
return $this->dateOption->get($input, $output);
}
}

View file

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Option;
use Cake\Chronos\Chronos;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function sprintf;
class StartDateOption
{
private readonly DateOption $dateOption;
public function __construct(Command $command, string $descriptionHint)
{
$this->dateOption = new DateOption($command, 'start-date', 's', sprintf(
'Allows to filter %s, returning only those older than provided date.',
$descriptionHint,
));
}
public function get(InputInterface $input, OutputInterface $output): ?Chronos
{
return $this->dateOption->get($input, $output);
}
}

View file

@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace ShlinkioCliTest\Shlink\CLI\Command;
use Shlinkio\Shlink\CLI\Command\ShortUrl\ListShortUrlsCommand;
use Shlinkio\Shlink\TestUtils\CliTest\CliTestCase;
class ListShortUrlsTest extends CliTestCase
{
/**
* @test
* @dataProvider provideFlagsAndOutput
*/
public function generatesExpectedOutput(array $flags, string $expectedOutput): void
{
[$output] = $this->exec([ListShortUrlsCommand::NAME, ...$flags], ['no']);
self::assertStringContainsString($expectedOutput, $output);
}
public function provideFlagsAndOutput(): iterable
{
// phpcs:disable Generic.Files.LineLength
yield 'no flags' => [[], <<<OUTPUT
+--------------------+---------------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+---------------+-------------------------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
| custom | | http://doma.in/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://doma.in/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
| abc123 | My cool title | http://doma.in/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
| ghi789 | | http://doma.in/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+--------------------+---------------+-------------------------------------------+---------------------------- Page 1 of 1 ------------------------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'start date' => [['--start-date=2019-01'], <<<OUTPUT
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+------------+-------+---------------------------+-----------------------------------------------------------------------------------------------------------+---------------------------+--------------+
| ghi789 | | http://example.com/ghi789 | https://blog.alejandrocelaya.com/2019/04/27/considerations-to-properly-use-open-source-software-projects/ | 2019-01-01T00:00:30+00:00 | 0 |
| custom | | http://doma.in/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://doma.in/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
+------------+-------+---------------------------+-------------------------------------------- Page 1 of 1 --------------------------------------------------+---------------------------+--------------+
OUTPUT];
yield 'end date' => [['-e 2018-12-01'], <<<OUTPUT
+--------------------+---------------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+---------------+-------------------------------------------+----------------------------------+---------------------------+--------------+
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
| abc123 | My cool title | http://doma.in/abc123 | https://shlink.io | 2018-05-01T00:00:00+00:00 | 3 |
| ghi789 | | http://doma.in/ghi789 | https://shlink.io/documentation/ | 2018-05-01T00:00:00+00:00 | 2 |
+--------------------+---------------+----------------------------------- Page 1 of 1 ------------------------------+---------------------------+--------------+
OUTPUT];
yield 'start and end date' => [['-s 2018-06-20', '--end-date=2019-01-01T00:00:20+00:00'], <<<OUTPUT
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| Short Code | Title | Short URL | Long URL | Date created | Visits count |
+--------------------+-------+-------------------------------------------+-----------------------------------------------------------------------------------------------------+---------------------------+--------------+
| custom | | http://doma.in/custom | https://shlink.io | 2019-01-01T00:00:20+00:00 | 0 |
| def456 | | http://doma.in/def456 | https://blog.alejandrocelaya.com/2017/12/09/acmailer-7-0-the-most-important-release-in-a-long-time/ | 2019-01-01T00:00:10+00:00 | 2 |
| custom-with-domain | | http://some-domain.com/custom-with-domain | https://google.com | 2018-10-20T00:00:00+00:00 | 0 |
+--------------------+-------+-------------------------------------------+----------------------------- Page 1 of 1 -----------------------------------------------------------+---------------------------+--------------+
OUTPUT];
// phpcs:enable
}
}

View file

@ -2,8 +2,6 @@
declare(strict_types=1);
// phpcs:disable
// TODO Enable coding style checks again once code sniffer 3.7 is released https://github.com/squizlabs/PHP_CodeSniffer/issues/3474
namespace Shlinkio\Shlink\Rest\ApiKey;
use Happyr\DoctrineSpecification\Spec;