Merge branch 'develop'

This commit is contained in:
Alejandro Celaya 2018-06-18 20:49:32 +02:00
commit a8465094c1
8 changed files with 85 additions and 35 deletions

View file

@ -1,5 +1,16 @@
## CHANGELOG ## CHANGELOG
### 1.9.1
**Bugs:**
* [154: When filtering by searchTerm, sizes of every result page has an unexpected behavior](https://github.com/shlinkio/shlink/issues/154)
* [157: Background commands executed by installation process do not respect the used php binary](https://github.com/shlinkio/shlink/issues/157)
**Enhancements:**
* [155: Improve the pagination object returned in lists, including more meaningful properties](https://github.com/shlinkio/shlink/issues/155)
### 1.9.0 ### 1.9.0
**Features** **Features**

View file

@ -3,11 +3,23 @@
"properties": { "properties": {
"currentPage": { "currentPage": {
"type": "integer", "type": "integer",
"description": "The number of current page being displayed." "description": "The number of current page."
}, },
"pagesCount": { "pagesCount": {
"type": "integer", "type": "integer",
"description": "The total number of pages that can be displayed." "description": "The total number of pages that can be obtained."
},
"itemsPerPage": {
"type": "integer",
"description": "The number of items for every page."
},
"itemsInCurrentPage": {
"type": "integer",
"description": "The number of items in current page (could be smaller than itemsPerPage)."
},
"totalItems": {
"type": "integer",
"description": "The total number of items among all pages."
} }
} }
} }

View file

@ -116,7 +116,10 @@
], ],
"pagination": { "pagination": {
"currentPage": 5, "currentPage": 5,
"pagesCount": 12 "pagesCount": 12,
"itemsPerPage": 10,
"itemsInCurrentPage": 10,
"totalItems": 115
} }
} }
} }

View file

@ -18,6 +18,7 @@ use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\PhpExecutableFinder;
use Zend\Config\Writer\WriterInterface; use Zend\Config\Writer\WriterInterface;
class InstallCommand extends Command class InstallCommand extends Command
@ -48,6 +49,14 @@ class InstallCommand extends Command
* @var bool * @var bool
*/ */
private $isUpdate; private $isUpdate;
/**
* @var PhpExecutableFinder
*/
private $phpFinder;
/**
* @var string|bool
*/
private $phpBinary;
/** /**
* InstallCommand constructor. * InstallCommand constructor.
@ -55,22 +64,25 @@ class InstallCommand extends Command
* @param Filesystem $filesystem * @param Filesystem $filesystem
* @param ConfigCustomizerManagerInterface $configCustomizers * @param ConfigCustomizerManagerInterface $configCustomizers
* @param bool $isUpdate * @param bool $isUpdate
* @param PhpExecutableFinder|null $phpFinder
* @throws LogicException * @throws LogicException
*/ */
public function __construct( public function __construct(
WriterInterface $configWriter, WriterInterface $configWriter,
Filesystem $filesystem, Filesystem $filesystem,
ConfigCustomizerManagerInterface $configCustomizers, ConfigCustomizerManagerInterface $configCustomizers,
$isUpdate = false bool $isUpdate = false,
PhpExecutableFinder $phpFinder = null
) { ) {
parent::__construct(); parent::__construct();
$this->configWriter = $configWriter; $this->configWriter = $configWriter;
$this->isUpdate = $isUpdate; $this->isUpdate = $isUpdate;
$this->filesystem = $filesystem; $this->filesystem = $filesystem;
$this->configCustomizers = $configCustomizers; $this->configCustomizers = $configCustomizers;
$this->phpFinder = $phpFinder ?: new PhpExecutableFinder();
} }
public function configure() protected function configure(): void
{ {
$this $this
->setName('shlink:install') ->setName('shlink:install')
@ -84,7 +96,7 @@ class InstallCommand extends Command
* @throws ContainerExceptionInterface * @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface * @throws NotFoundExceptionInterface
*/ */
public function execute(InputInterface $input, OutputInterface $output) protected function execute(InputInterface $input, OutputInterface $output): void
{ {
$this->io = new SymfonyStyle($input, $output); $this->io = new SymfonyStyle($input, $output);
@ -133,8 +145,8 @@ class InstallCommand extends Command
// If current command is not update, generate database // If current command is not update, generate database
if (! $this->isUpdate) { if (! $this->isUpdate) {
$this->io->write('Initializing database...'); $this->io->write('Initializing database...');
if (! $this->runCommand( if (! $this->runPhpCommand(
'php vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create', 'vendor/doctrine/orm/bin/doctrine.php orm:schema-tool:create',
'Error generating database.', 'Error generating database.',
$output $output
)) { )) {
@ -144,8 +156,8 @@ class InstallCommand extends Command
// Run database migrations // Run database migrations
$this->io->write('Updating database...'); $this->io->write('Updating database...');
if (! $this->runCommand( if (! $this->runPhpCommand(
'php vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate', 'vendor/doctrine/migrations/bin/doctrine-migrations.php migrations:migrate',
'Error updating database.', 'Error updating database.',
$output $output
)) { )) {
@ -154,8 +166,8 @@ class InstallCommand extends Command
// Generate proxies // Generate proxies
$this->io->write('Generating proxies...'); $this->io->write('Generating proxies...');
if (! $this->runCommand( if (! $this->runPhpCommand(
'php vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies', 'vendor/doctrine/orm/bin/doctrine.php orm:generate-proxies',
'Error generating proxies.', 'Error generating proxies.',
$output $output
)) { )) {
@ -213,23 +225,27 @@ class InstallCommand extends Command
* @throws LogicException * @throws LogicException
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
private function runCommand($command, $errorMessage, OutputInterface $output): bool private function runPhpCommand($command, $errorMessage, OutputInterface $output): bool
{ {
if ($this->processHelper === null) { if ($this->processHelper === null) {
$this->processHelper = $this->getHelper('process'); $this->processHelper = $this->getHelper('process');
} }
$process = $this->processHelper->run($output, $command); if ($this->phpBinary === null) {
$this->phpBinary = $this->phpFinder->find(false) ?: 'php';
}
$this->io->writeln('Running "' . sprintf('%s %s', $this->phpBinary, $command) . '"');
$process = $this->processHelper->run($output, sprintf('%s %s', $this->phpBinary, $command));
if ($process->isSuccessful()) { if ($process->isSuccessful()) {
$this->io->writeln(' <info>Success!</info>'); $this->io->writeln(' <info>Success!</info>');
return true; return true;
} }
if ($this->io->isVerbose()) { if (! $this->io->isVerbose()) {
return false; $this->io->error($errorMessage . ' Run this command with -vvv to see specific error info.');
} }
$this->io->error($errorMessage . ' Run this command with -vvv to see specific error info.');
return false; return false;
} }
} }

View file

@ -15,6 +15,7 @@ use Symfony\Component\Console\Helper\ProcessHelper;
use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Console\Tester\CommandTester;
use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\PhpExecutableFinder;
use Symfony\Component\Process\Process; use Symfony\Component\Process\Process;
use Zend\Config\Writer\WriterInterface; use Zend\Config\Writer\WriterInterface;
@ -55,6 +56,9 @@ class InstallCommandTest extends TestCase
$configCustomizers = $this->prophesize(ConfigCustomizerManagerInterface::class); $configCustomizers = $this->prophesize(ConfigCustomizerManagerInterface::class);
$configCustomizers->get(Argument::cetera())->willReturn($configCustomizer->reveal()); $configCustomizers->get(Argument::cetera())->willReturn($configCustomizer->reveal());
$finder = $this->prophesize(PhpExecutableFinder::class);
$finder->find(false)->willReturn('php');
$app = new Application(); $app = new Application();
$helperSet = $app->getHelperSet(); $helperSet = $app->getHelperSet();
$helperSet->set($processHelper->reveal()); $helperSet->set($processHelper->reveal());
@ -62,7 +66,9 @@ class InstallCommandTest extends TestCase
$this->command = new InstallCommand( $this->command = new InstallCommand(
$this->configWriter->reveal(), $this->configWriter->reveal(),
$this->filesystem->reveal(), $this->filesystem->reveal(),
$configCustomizers->reveal() $configCustomizers->reveal(),
false,
$finder->reveal()
); );
$app->add($this->command); $app->add($this->command);

View file

@ -8,7 +8,7 @@ use Zend\Paginator\Adapter\AdapterInterface;
class PaginableRepositoryAdapter implements AdapterInterface class PaginableRepositoryAdapter implements AdapterInterface
{ {
const ITEMS_PER_PAGE = 10; public const ITEMS_PER_PAGE = 10;
/** /**
* @var PaginableRepositoryInterface * @var PaginableRepositoryInterface
@ -34,7 +34,7 @@ class PaginableRepositoryAdapter implements AdapterInterface
$orderBy = null $orderBy = null
) { ) {
$this->paginableRepository = $paginableRepository; $this->paginableRepository = $paginableRepository;
$this->searchTerm = $searchTerm !== null ? trim(strip_tags($searchTerm)) : null; $this->searchTerm = $searchTerm !== null ? \trim(\strip_tags($searchTerm)) : null;
$this->orderBy = $orderBy; $this->orderBy = $orderBy;
$this->tags = $tags; $this->tags = $tags;
} }
@ -46,7 +46,7 @@ class PaginableRepositoryAdapter implements AdapterInterface
* @param int $itemCountPerPage Number of items per page * @param int $itemCountPerPage Number of items per page
* @return array * @return array
*/ */
public function getItems($offset, $itemCountPerPage) public function getItems($offset, $itemCountPerPage): array
{ {
return $this->paginableRepository->findList( return $this->paginableRepository->findList(
$itemCountPerPage, $itemCountPerPage,
@ -66,7 +66,7 @@ class PaginableRepositoryAdapter implements AdapterInterface
* The return value is cast to an integer. * The return value is cast to an integer.
* @since 5.1.0 * @since 5.1.0
*/ */
public function count() public function count(): int
{ {
return $this->paginableRepository->countList($this->searchTerm, $this->tags); return $this->paginableRepository->countList($this->searchTerm, $this->tags);
} }

View file

@ -8,13 +8,16 @@ use Zend\Stdlib\ArrayUtils;
trait PaginatorUtilsTrait trait PaginatorUtilsTrait
{ {
protected function serializePaginator(Paginator $paginator) protected function serializePaginator(Paginator $paginator): array
{ {
return [ return [
'data' => ArrayUtils::iteratorToArray($paginator->getCurrentItems()), 'data' => ArrayUtils::iteratorToArray($paginator->getCurrentItems()),
'pagination' => [ 'pagination' => [
'currentPage' => $paginator->getCurrentPageNumber(), 'currentPage' => $paginator->getCurrentPageNumber(),
'pagesCount' => $paginator->count(), 'pagesCount' => $paginator->count(),
'itemsPerPage' => $paginator->getItemCountPerPage(),
'itemsInCurrentPage' => $paginator->getCurrentItemCount(),
'totalItems' => $paginator->getTotalItemCount(),
], ],
]; ];
} }
@ -25,7 +28,7 @@ trait PaginatorUtilsTrait
* @param Paginator $paginator * @param Paginator $paginator
* @return bool * @return bool
*/ */
protected function isLastPage(Paginator $paginator) protected function isLastPage(Paginator $paginator): bool
{ {
return $paginator->getCurrentPageNumber() >= $paginator->count(); return $paginator->getCurrentPageNumber() >= $paginator->count();
} }

View file

@ -25,7 +25,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
$orderBy = null $orderBy = null
): array { ): array {
$qb = $this->createListQueryBuilder($searchTerm, $tags); $qb = $this->createListQueryBuilder($searchTerm, $tags);
$qb->select('s'); $qb->select('DISTINCT s');
// Set limit and offset // Set limit and offset
if ($limit !== null) { if ($limit !== null) {
@ -47,17 +47,17 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
protected function processOrderByForList(QueryBuilder $qb, $orderBy) protected function processOrderByForList(QueryBuilder $qb, $orderBy)
{ {
$fieldName = is_array($orderBy) ? key($orderBy) : $orderBy; $fieldName = \is_array($orderBy) ? \key($orderBy) : $orderBy;
$order = is_array($orderBy) ? $orderBy[$fieldName] : 'ASC'; $order = \is_array($orderBy) ? $orderBy[$fieldName] : 'ASC';
if (in_array($fieldName, ['visits', 'visitsCount', 'visitCount'], true)) { if (\in_array($fieldName, ['visits', 'visitsCount', 'visitCount'], true)) {
$qb->addSelect('COUNT(v) AS totalVisits') $qb->addSelect('COUNT(v) AS totalVisits')
->leftJoin('s.visits', 'v') ->leftJoin('s.visits', 'v')
->groupBy('s') ->groupBy('s')
->orderBy('totalVisits', $order); ->orderBy('totalVisits', $order);
return array_column($qb->getQuery()->getResult(), 0); return \array_column($qb->getQuery()->getResult(), 0);
} elseif (in_array($fieldName, ['originalUrl', 'shortCode', 'dateCreated'], true)) { } elseif (\in_array($fieldName, ['originalUrl', 'shortCode', 'dateCreated'], true)) {
$qb->orderBy('s.' . $fieldName, $order); $qb->orderBy('s.' . $fieldName, $order);
} }
@ -74,7 +74,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
public function countList(string $searchTerm = null, array $tags = []): int public function countList(string $searchTerm = null, array $tags = []): int
{ {
$qb = $this->createListQueryBuilder($searchTerm, $tags); $qb = $this->createListQueryBuilder($searchTerm, $tags);
$qb->select('COUNT(s)'); $qb->select('COUNT(DISTINCT s)');
return (int) $qb->getQuery()->getSingleScalarResult(); return (int) $qb->getQuery()->getSingleScalarResult();
} }
@ -92,7 +92,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
// Apply search term to every searchable field if not empty // Apply search term to every searchable field if not empty
if (! empty($searchTerm)) { if (! empty($searchTerm)) {
$qb->join('s.tags', 't'); $qb->leftJoin('s.tags', 't');
$conditions = [ $conditions = [
$qb->expr()->like('s.originalUrl', ':searchPattern'), $qb->expr()->like('s.originalUrl', ':searchPattern'),
@ -102,8 +102,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
// Unpack and apply search conditions // Unpack and apply search conditions
$qb->andWhere($qb->expr()->orX(...$conditions)); $qb->andWhere($qb->expr()->orX(...$conditions));
$searchTerm = '%' . $searchTerm . '%'; $qb->setParameter('searchPattern', '%' . $searchTerm . '%');
$qb->setParameter('searchPattern', $searchTerm);
} }
// Filter by tags if provided // Filter by tags if provided
@ -119,7 +118,7 @@ class ShortUrlRepository extends EntityRepository implements ShortUrlRepositoryI
* @param string $shortCode * @param string $shortCode
* @return ShortUrl|null * @return ShortUrl|null
*/ */
public function findOneByShortCode(string $shortCode) public function findOneByShortCode(string $shortCode): ?ShortUrl
{ {
$now = new \DateTimeImmutable(); $now = new \DateTimeImmutable();