Merge pull request #316 from acelaya/feature/symfony-42

Feature/symfony 42
This commit is contained in:
Alejandro Celaya 2018-12-08 14:30:20 +01:00 committed by GitHub
commit 73605414f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 259 additions and 55 deletions

View file

@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
#### Changed
* [#312](https://github.com/shlinkio/shlink/issues/312) Now all config files both in `php` and `json` format are loaded from `config/params` folder, easing users to provided customizations to docker image.
* [#226](https://github.com/shlinkio/shlink/issues/226) Updated how table are rendered in CLI commands, making use of new features in Symfony 4.2.
#### Deprecated

View file

@ -30,10 +30,10 @@
"mikehaertl/phpwkhtmltopdf": "^2.2",
"monolog/monolog": "^1.21",
"roave/security-advisories": "dev-master",
"symfony/console": "^4.1",
"symfony/filesystem": "^4.1",
"symfony/lock": "^4.1",
"symfony/process": "^4.1",
"symfony/console": "^4.2",
"symfony/filesystem": "^4.2",
"symfony/lock": "^4.2",
"symfony/process": "^4.2",
"theorchard/monolog-cascade": "^0.4",
"zendframework/zend-config": "^3.0",
"zendframework/zend-config-aggregator": "^1.0",
@ -57,8 +57,8 @@
"phpunit/phpcov": "^5.0",
"phpunit/phpunit": "^7.3",
"shlinkio/php-coding-standard": "~1.0.0",
"symfony/dotenv": "^4.0",
"symfony/var-dumper": "^4.0",
"symfony/dotenv": "^4.2",
"symfony/var-dumper": "^4.2",
"zendframework/zend-component-installer": "^2.1",
"zendframework/zend-expressive-tooling": "^1.0"
},

View file

@ -26,7 +26,7 @@ $config['entity_manager']['connection'] = [
$sm->setService('config', $config);
// Create database
$process = new Process('vendor/bin/doctrine orm:schema-tool:create --no-interaction -q --test', __DIR__);
$process = new Process(['vendor/bin/doctrine', 'orm:schema-tool:create', '--no-interaction', '-q', '--test'], __DIR__);
$process->inheritEnvironmentVariables()
->mustRun();

View file

@ -3,13 +3,13 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
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 array_filter;
use function array_map;
use function sprintf;
@ -46,7 +46,6 @@ class ListKeysCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): void
{
$io = new SymfonyStyle($input, $output);
$enabledOnly = $input->getOption('enabledOnly');
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) {
@ -62,7 +61,7 @@ class ListKeysCommand extends Command
return $rowData;
}, $this->apiKeyService->listKeys($enabledOnly));
$io->table(array_filter([
ShlinkTable::fromOutput($output)->render(array_filter([
'Key',
! $enabledOnly ? 'Is enabled' : null,
'Expiration date',

View file

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Cake\Chronos\Chronos;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\Common\Util\DateRange;
use Shlinkio\Shlink\Core\Entity\Visit;
use Shlinkio\Shlink\Core\Model\VisitsParams;
@ -69,7 +70,6 @@ class GetVisitsCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): void
{
$io = new SymfonyStyle($input, $output);
$shortCode = $input->getArgument('shortCode');
$startDate = $this->getDateOption($input, 'startDate');
$endDate = $this->getDateOption($input, 'endDate');
@ -82,7 +82,7 @@ class GetVisitsCommand extends Command
$rowData['country'] = $visit->getVisitLocation()->getCountryName();
return select_keys($rowData, ['referer', 'date', 'userAgent', 'country']);
}, $visits);
$io->table(['Referer', 'Date', 'User agent', 'Country'], $rows);
ShlinkTable::fromOutput($output)->render(['Referer', 'Date', 'User agent', 'Country'], $rows);
}
private function getDateOption(InputInterface $input, $key)

View file

@ -3,8 +3,10 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\ShortUrl;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\Common\Paginator\Adapter\PaginableRepositoryAdapter;
use Shlinkio\Shlink\Common\Paginator\Util\PaginatorUtilsTrait;
use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Shlinkio\Shlink\Core\Service\ShortUrlServiceInterface;
use Shlinkio\Shlink\Core\Transformer\ShortUrlDataTransformer;
use Symfony\Component\Console\Command\Command;
@ -12,6 +14,7 @@ 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 Zend\Paginator\Paginator;
use function array_values;
use function count;
use function explode;
@ -78,39 +81,56 @@ class ListShortUrlsCommand extends Command
$searchTerm = $input->getOption('searchTerm');
$tags = $input->getOption('tags');
$tags = ! empty($tags) ? explode(',', $tags) : [];
$showTags = $input->getOption('showTags');
$showTags = (bool) $input->getOption('showTags');
$transformer = new ShortUrlDataTransformer($this->domainConfig);
do {
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
$result = $this->renderPage($input, $output, $page, $searchTerm, $tags, $showTags, $transformer);
$page++;
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
if ($showTags) {
$headers[] = 'Tags';
}
$rows = [];
foreach ($result as $row) {
$shortUrl = $transformer->transform($row);
if ($showTags) {
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
} else {
unset($shortUrl['tags']);
}
unset($shortUrl['originalUrl']);
$rows[] = array_values($shortUrl);
}
$io->table($headers, $rows);
if ($this->isLastPage($result)) {
$continue = false;
$io->success('Short URLs properly listed');
} else {
$continue = $io->confirm(sprintf('Continue with page <options=bold>%s</>?', $page), false);
}
$continue = $this->isLastPage($result)
? false
: $io->confirm(sprintf('Continue with page <options=bold>%s</>?', $page), false);
} while ($continue);
$io->newLine();
$io->success('Short URLs properly listed');
}
private function renderPage(
InputInterface $input,
OutputInterface $output,
int $page,
?string $searchTerm,
array $tags,
bool $showTags,
DataTransformerInterface $transformer
): Paginator {
$result = $this->shortUrlService->listShortUrls($page, $searchTerm, $tags, $this->processOrderBy($input));
$headers = ['Short code', 'Short URL', 'Long URL', 'Date created', 'Visits count'];
if ($showTags) {
$headers[] = 'Tags';
}
$rows = [];
foreach ($result as $row) {
$shortUrl = $transformer->transform($row);
if ($showTags) {
$shortUrl['tags'] = implode(', ', $shortUrl['tags']);
} else {
unset($shortUrl['tags']);
}
unset($shortUrl['originalUrl']);
$rows[] = array_values($shortUrl);
}
ShlinkTable::fromOutput($output)->render($headers, $rows, $this->formatCurrentPageMessage(
$result,
'Page %s of %s'
));
return $result;
}
private function processOrderBy(InputInterface $input)

View file

@ -3,12 +3,12 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\CLI\Command\Tag;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Shlinkio\Shlink\Core\Entity\Tag;
use Shlinkio\Shlink\Core\Service\Tag\TagServiceInterface;
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 Functional\map;
class ListTagsCommand extends Command
@ -33,8 +33,7 @@ class ListTagsCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output): void
{
$io = new SymfonyStyle($input, $output);
$io->table(['Name'], $this->getTagsRows());
ShlinkTable::fromOutput($output)->render(['Name'], $this->getTagsRows());
}
private function getTagsRows(): array

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Common\Console;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Output\OutputInterface;
final class ShlinkTable
{
private const DEFAULT_STYLE_NAME = 'default';
private const TABLE_TITLE_STYLE = '<options=bold> %s </>';
/** @var Table|null */
private $baseTable;
public function __construct(Table $baseTable)
{
$this->baseTable = $baseTable;
}
public static function fromOutput(OutputInterface $output): self
{
return new self(new Table($output));
}
public function render(array $headers, array $rows, ?string $footerTitle = null, ?string $headerTitle = null): void
{
$style = Table::getStyleDefinition(self::DEFAULT_STYLE_NAME);
$style->setFooterTitleFormat(self::TABLE_TITLE_STYLE)
->setHeaderTitleFormat(self::TABLE_TITLE_STYLE);
$table = clone $this->baseTable;
$table->setStyle($style)
->setHeaders($headers)
->setRows($rows)
->setFooterTitle($footerTitle)
->setHeaderTitle($headerTitle)
->render();
}
}

View file

@ -7,6 +7,7 @@ use Shlinkio\Shlink\Common\Rest\DataTransformerInterface;
use Zend\Paginator\Paginator;
use Zend\Stdlib\ArrayUtils;
use function array_map;
use function sprintf;
trait PaginatorUtilsTrait
{
@ -39,4 +40,9 @@ trait PaginatorUtilsTrait
{
return $paginator->getCurrentPageNumber() >= $paginator->count();
}
private function formatCurrentPageMessage(Paginator $paginator, string $pattern): string
{
return sprintf($pattern, $paginator->getCurrentPageNumber(), $paginator->count());
}
}

View file

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace ShlinkioTest\Shlink\Common\Console;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\Prophecy\ObjectProphecy;
use ReflectionObject;
use Shlinkio\Shlink\Common\Console\ShlinkTable;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableStyle;
use Symfony\Component\Console\Output\OutputInterface;
class ShlinkTableTest extends TestCase
{
/** @var ShlinkTable */
private $shlinkTable;
/** @var ObjectProphecy */
private $baseTable;
public function setUp()
{
$this->baseTable = $this->prophesize(Table::class);
$this->shlinkTable = new ShlinkTable($this->baseTable->reveal());
}
/**
* @test
*/
public function renderMakesTableToBeRenderedWithProvidedInfo()
{
$headers = [];
$rows = [[]];
$headerTitle = 'Header';
$footerTitle = 'Footer';
$setStyle = $this->baseTable->setStyle(Argument::type(TableStyle::class))->willReturn(
$this->baseTable->reveal()
);
$setHeaders = $this->baseTable->setHeaders($headers)->willReturn($this->baseTable->reveal());
$setRows = $this->baseTable->setRows($rows)->willReturn($this->baseTable->reveal());
$setFooterTitle = $this->baseTable->setFooterTitle($footerTitle)->willReturn($this->baseTable->reveal());
$setHeaderTitle = $this->baseTable->setHeaderTitle($headerTitle)->willReturn($this->baseTable->reveal());
$render = $this->baseTable->render()->willReturn($this->baseTable->reveal());
$this->shlinkTable->render($headers, $rows, $footerTitle, $headerTitle);
$setStyle->shouldHaveBeenCalledOnce();
$setHeaders->shouldHaveBeenCalledOnce();
$setRows->shouldHaveBeenCalledOnce();
$setFooterTitle->shouldHaveBeenCalledOnce();
$setHeaderTitle->shouldHaveBeenCalledOnce();
$render->shouldHaveBeenCalledOnce();
}
/**
* @test
*/
public function newTableIsCreatedForFactoryMethod()
{
$instance = ShlinkTable::fromOutput($this->prophesize(OutputInterface::class)->reveal());
$ref = new ReflectionObject($instance);
$baseTable = $ref->getProperty('baseTable');
$baseTable->setAccessible(true);
$this->assertInstanceOf(Table::class, $baseTable->getValue($instance));
}
}

View file

@ -11,6 +11,8 @@ use Shlinkio\Shlink\Common\Exception\WrongIpException;
use Shlinkio\Shlink\Common\Util\IpAddress;
use Shlinkio\Shlink\Core\Model\Visitor;
use Shlinkio\Shlink\Core\Repository\VisitRepository;
use Shlinkio\Shlink\Core\Visit\Model\UnknownVisitLocation;
use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface;
/**
* Class Visit
@ -88,9 +90,9 @@ class Visit extends AbstractEntity implements JsonSerializable
return ! empty($this->remoteAddr);
}
public function getVisitLocation(): VisitLocation
public function getVisitLocation(): VisitLocationInterface
{
return $this->visitLocation;
return $this->visitLocation ?? new UnknownVisitLocation();
}
public function locate(VisitLocation $visitLocation): self

View file

@ -4,8 +4,8 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Entity;
use Doctrine\ORM\Mapping as ORM;
use JsonSerializable;
use Shlinkio\Shlink\Common\Entity\AbstractEntity;
use Shlinkio\Shlink\Core\Visit\Model\VisitLocationInterface;
use function array_key_exists;
/**
@ -16,7 +16,7 @@ use function array_key_exists;
* @ORM\Entity()
* @ORM\Table(name="visit_locations")
*/
class VisitLocation extends AbstractEntity implements JsonSerializable
class VisitLocation extends AbstractEntity implements VisitLocationInterface
{
/**
* @var string

View file

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Model;
final class UnknownVisitLocation implements VisitLocationInterface
{
public function getCountryName(): string
{
return 'Unknown';
}
public function getLatitude(): string
{
return '0.0';
}
public function getLongitude(): string
{
return '0.0';
}
public function getCityName(): string
{
return 'Unknown';
}
/**
* Specify data which should be serialized to JSON
* @link https://php.net/manual/en/jsonserializable.jsonserialize.php
* @return mixed data which can be serialized by <b>json_encode</b>,
* which is a value of any type other than a resource.
* @since 5.4.0
*/
public function jsonSerialize()
{
return [
'countryCode' => 'Unknown',
'countryName' => 'Unknown',
'regionName' => 'Unknown',
'cityName' => 'Unknown',
'latitude' => '0.0',
'longitude' => '0.0',
'timezone' => 'Unknown',
];
}
}

View file

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Core\Visit\Model;
use JsonSerializable;
interface VisitLocationInterface extends JsonSerializable
{
public function getCountryName(): string;
public function getLatitude(): string;
public function getLongitude(): string;
public function getCityName(): string;
}

View file

@ -20,7 +20,8 @@ use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\PhpExecutableFinder;
use Zend\Config\Writer\WriterInterface;
use function sprintf;
use function array_unshift;
use function implode;
class InstallCommand extends Command
{
@ -133,7 +134,7 @@ class InstallCommand extends Command
if (! $this->isUpdate) {
$this->io->write('Initializing database...');
if (! $this->execPhp(
'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
['vendor/doctrine/orm/bin/doctrine.php', 'orm:schema-tool:create'],
'Error generating database.',
$output
)) {
@ -144,7 +145,7 @@ class InstallCommand extends Command
// Run database migrations
$this->io->write('Updating database...');
if (! $this->execPhp(
'vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate',
['vendor/doctrine/migrations/bin/doctrine-migrations.php', 'migrations:migrate'],
'Error updating database.',
$output
)) {
@ -154,16 +155,16 @@ class InstallCommand extends Command
// Generate proxies
$this->io->write('Generating proxies...');
if (! $this->execPhp(
'vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies',
['vendor/doctrine/orm/bin/doctrine.php', 'orm:generate-proxies'],
'Error generating proxies.',
$output
)) {
return;
}
// Download GeoLite2 db filte
// Download GeoLite2 db file
$this->io->write('Downloading GeoLite2 db...');
if (! $this->execPhp('bin/cli visit:update-db', 'Error downloading GeoLite2 db.', $output)) {
if (! $this->execPhp(['bin/cli', 'visit:update-db'], 'Error downloading GeoLite2 db.', $output)) {
return;
}
@ -215,7 +216,7 @@ class InstallCommand extends Command
return $config;
}
private function execPhp(string $command, string $errorMessage, OutputInterface $output): bool
private function execPhp(array $command, string $errorMessage, OutputInterface $output): bool
{
if ($this->processHelper === null) {
$this->processHelper = $this->getHelper('process');
@ -225,12 +226,13 @@ class InstallCommand extends Command
$this->phpBinary = $this->phpFinder->find(false) ?: 'php';
}
array_unshift($command, $this->phpBinary);
$this->io->write(
' <options=bold>[Running "' . sprintf('%s %s', $this->phpBinary, $command) . '"]</> ',
' <options=bold>[Running "' . implode(' ', $command) . '"]</> ',
false,
OutputInterface::VERBOSITY_VERBOSE
);
$process = $this->processHelper->run($output, sprintf('%s %s', $this->phpBinary, $command));
$process = $this->processHelper->run($output, $command);
if ($process->isSuccessful()) {
$this->io->writeln(' <info>Success!</info>');
return true;