diff --git a/src/Cli.php b/src/Cli.php
index 8a299c3b..e8c4bd3d 100644
--- a/src/Cli.php
+++ b/src/Cli.php
@@ -34,11 +34,19 @@ class Cli extends Application
}
$definition->addOption(
- new InputOption('with-context', null, InputOption::VALUE_NEGATABLE, 'Add context to output messages.')
+ new InputOption('context', null, InputOption::VALUE_NEGATABLE, 'Add context to output messages.')
);
$definition->addOption(new InputOption('trace', null, InputOption::VALUE_NONE, 'Enable tracing mode.'));
+ $definition->addOption(
+ new InputOption(
+ 'output', 'o', InputOption::VALUE_REQUIRED,
+ sprintf('Output mode. Can be [%s].', implode(', ', Command::DISPLAY_OUTPUT)),
+ Command::DISPLAY_OUTPUT[0]
+ )
+ );
+
return $definition;
}
}
diff --git a/src/Command.php b/src/Command.php
index 420c063f..fb21a203 100644
--- a/src/Command.php
+++ b/src/Command.php
@@ -23,7 +23,7 @@ class Command extends BaseCommand
{
use LockableTrait;
- protected array $outputs = [
+ public const DISPLAY_OUTPUT = [
'table',
'json',
'yaml',
@@ -31,11 +31,11 @@ class Command extends BaseCommand
protected function execute(InputInterface $input, OutputInterface $output): int
{
- if ($input->hasOption('with-context') && true === $input->getOption('with-context')) {
+ if ($input->hasOption('context') && true === $input->getOption('context')) {
Config::save('logs.context', true);
}
- if ($input->hasOption('no-with-context') && true === $input->getOption('no-with-context')) {
+ if ($input->hasOption('no-context') && true === $input->getOption('no-context')) {
Config::save('logs.context', false);
}
@@ -249,5 +249,19 @@ class Command extends BaseCommand
$suggestions->suggestValues($suggest);
}
+
+ if ($input->mustSuggestOptionValuesFor('output')) {
+ $currentValue = $input->getCompletionValue();
+
+ $suggest = [];
+
+ foreach (self::DISPLAY_OUTPUT as $name) {
+ if (empty($currentValue) || str_starts_with($name, $currentValue)) {
+ $suggest[] = $name;
+ }
+ }
+
+ $suggestions->suggestValues($suggest);
+ }
}
}
diff --git a/src/Commands/Backend/Library/ListCommand.php b/src/Commands/Backend/Library/ListCommand.php
index 8ce0043d..304e7fa9 100644
--- a/src/Commands/Backend/Library/ListCommand.php
+++ b/src/Commands/Backend/Library/ListCommand.php
@@ -8,8 +8,6 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Options;
use RuntimeException;
-use Symfony\Component\Console\Completion\CompletionInput;
-use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -22,13 +20,6 @@ final class ListCommand extends Command
{
$this->setName('backend:library:list')
->setDescription('Get Backend libraries list.')
- ->addOption(
- 'output',
- 'o',
- InputOption::VALUE_OPTIONAL,
- sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
- $this->outputs[0],
- )
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
->addArgument('backend', InputArgument::REQUIRED, 'Backend name.');
@@ -102,29 +93,4 @@ final class ListCommand extends Command
return self::FAILURE;
}
}
-
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- parent::complete($input, $suggestions);
-
- $methods = [
- 'output' => 'outputs',
- ];
-
- foreach ($methods as $key => $of) {
- if ($input->mustSuggestOptionValuesFor($key)) {
- $currentValue = $input->getCompletionValue();
-
- $suggest = [];
-
- foreach ($this->{$of} as $name) {
- if (empty($currentValue) || str_starts_with($name, $currentValue)) {
- $suggest[] = $name;
- }
- }
-
- $suggestions->suggestValues($suggest);
- }
- }
- }
}
diff --git a/src/Commands/Backend/Library/MismatchCommand.php b/src/Commands/Backend/Library/MismatchCommand.php
index 110bcb69..744c0e76 100644
--- a/src/Commands/Backend/Library/MismatchCommand.php
+++ b/src/Commands/Backend/Library/MismatchCommand.php
@@ -30,16 +30,6 @@ final class MismatchCommand extends Command
->setDescription(
'Find possible mis-identified item in a library. This only works for Media that follow Plex naming format.'
)
- ->addOption(
- 'output',
- 'o',
- InputOption::VALUE_OPTIONAL,
- sprintf(
- 'Output mode. Can be [%s]. Modes other than table mode gives more info.',
- implode(', ', $this->outputs)
- ),
- $this->outputs[0],
- )
->addOption('percentage', 'p', InputOption::VALUE_OPTIONAL, 'Acceptable percentage.', 50.0)
->addOption(
'method',
@@ -269,7 +259,6 @@ final class MismatchCommand extends Command
parent::complete($input, $suggestions);
$methods = [
- 'output' => 'outputs',
'method' => 'methods',
];
diff --git a/src/Commands/Backend/Library/UnmatchedCommand.php b/src/Commands/Backend/Library/UnmatchedCommand.php
index ba2f24c9..d69d6804 100644
--- a/src/Commands/Backend/Library/UnmatchedCommand.php
+++ b/src/Commands/Backend/Library/UnmatchedCommand.php
@@ -8,8 +8,6 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Options;
use RuntimeException;
-use Symfony\Component\Console\Completion\CompletionInput;
-use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -24,16 +22,6 @@ final class UnmatchedCommand extends Command
$this->setName('backend:library:unmatched')
->setDescription('Find top level Items in library that has no external ids.')
->addOption('show-all', null, InputOption::VALUE_NONE, 'Show all items regardless of the match status.')
- ->addOption(
- 'output',
- 'o',
- InputOption::VALUE_OPTIONAL,
- sprintf(
- 'Output mode. Can be [%s]. Modes other than table mode gives more info.',
- implode(', ', $this->outputs)
- ),
- $this->outputs[0],
- )
->addOption(
'timeout',
null,
@@ -151,29 +139,4 @@ final class UnmatchedCommand extends Command
return self::SUCCESS;
}
-
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- parent::complete($input, $suggestions);
-
- $methods = [
- 'output' => 'outputs',
- ];
-
- foreach ($methods as $key => $of) {
- if ($input->mustSuggestOptionValuesFor($key)) {
- $currentValue = $input->getCompletionValue();
-
- $suggest = [];
-
- foreach ($this->{$of} as $name) {
- if (empty($currentValue) || str_starts_with($name, $currentValue)) {
- $suggest[] = $name;
- }
- }
-
- $suggestions->suggestValues($suggest);
- }
- }
- }
}
diff --git a/src/Commands/Backend/Search/IdCommand.php b/src/Commands/Backend/Search/IdCommand.php
index 977c5827..b9d5aab6 100644
--- a/src/Commands/Backend/Search/IdCommand.php
+++ b/src/Commands/Backend/Search/IdCommand.php
@@ -8,8 +8,6 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Options;
use RuntimeException;
-use Symfony\Component\Console\Completion\CompletionInput;
-use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -22,13 +20,6 @@ final class IdCommand extends Command
{
$this->setName('backend:search:id')
->setDescription('Get backend metadata related to specific id.')
- ->addOption(
- 'output',
- 'o',
- InputOption::VALUE_OPTIONAL,
- sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
- $this->outputs[0],
- )
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
->addArgument('backend', InputArgument::REQUIRED, 'Backend name.')
@@ -89,29 +80,4 @@ final class IdCommand extends Command
return self::FAILURE;
}
}
-
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- parent::complete($input, $suggestions);
-
- $methods = [
- 'output' => 'outputs',
- ];
-
- foreach ($methods as $key => $of) {
- if ($input->mustSuggestOptionValuesFor($key)) {
- $currentValue = $input->getCompletionValue();
-
- $suggest = [];
-
- foreach ($this->{$of} as $name) {
- if (empty($currentValue) || str_starts_with($name, $currentValue)) {
- $suggest[] = $name;
- }
- }
-
- $suggestions->suggestValues($suggest);
- }
- }
- }
}
diff --git a/src/Commands/Backend/Search/QueryCommand.php b/src/Commands/Backend/Search/QueryCommand.php
index e0e173a3..e161f7cb 100644
--- a/src/Commands/Backend/Search/QueryCommand.php
+++ b/src/Commands/Backend/Search/QueryCommand.php
@@ -8,8 +8,6 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Options;
use RuntimeException;
-use Symfony\Component\Console\Completion\CompletionInput;
-use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -22,13 +20,6 @@ final class QueryCommand extends Command
{
$this->setName('backend:search:query')
->setDescription('Search backend libraries for specific title keyword.')
- ->addOption(
- 'output',
- 'o',
- InputOption::VALUE_OPTIONAL,
- sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
- $this->outputs[0],
- )
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
->addOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limit returned results.', 25)
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
@@ -94,29 +85,4 @@ final class QueryCommand extends Command
return self::FAILURE;
}
}
-
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- parent::complete($input, $suggestions);
-
- $methods = [
- 'output' => 'outputs',
- ];
-
- foreach ($methods as $key => $of) {
- if ($input->mustSuggestOptionValuesFor($key)) {
- $currentValue = $input->getCompletionValue();
-
- $suggest = [];
-
- foreach ($this->{$of} as $name) {
- if (empty($currentValue) || str_starts_with($name, $currentValue)) {
- $suggest[] = $name;
- }
- }
-
- $suggestions->suggestValues($suggest);
- }
- }
- }
}
diff --git a/src/Commands/Backend/Users/ListCommand.php b/src/Commands/Backend/Users/ListCommand.php
index 4db7eba8..fd34d058 100644
--- a/src/Commands/Backend/Users/ListCommand.php
+++ b/src/Commands/Backend/Users/ListCommand.php
@@ -8,8 +8,6 @@ use App\Command;
use App\Libs\Config;
use App\Libs\Options;
use RuntimeException;
-use Symfony\Component\Console\Completion\CompletionInput;
-use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@@ -23,13 +21,6 @@ final class ListCommand extends Command
{
$this->setName('backend:users:list')
->setDescription('Get backend users list.')
- ->addOption(
- 'output',
- 'o',
- InputOption::VALUE_OPTIONAL,
- sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
- $this->outputs[0],
- )
->addOption('with-tokens', 't', InputOption::VALUE_NONE, 'Include access tokens in response.')
->addOption('include-raw-response', null, InputOption::VALUE_NONE, 'Include unfiltered raw response.')
->addOption('config', 'c', InputOption::VALUE_REQUIRED, 'Use Alternative config file.')
@@ -108,29 +99,4 @@ final class ListCommand extends Command
return self::FAILURE;
}
}
-
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- parent::complete($input, $suggestions);
-
- $methods = [
- 'output' => 'outputs',
- ];
-
- foreach ($methods as $key => $of) {
- if ($input->mustSuggestOptionValuesFor($key)) {
- $currentValue = $input->getCompletionValue();
-
- $suggest = [];
-
- foreach ($this->{$of} as $name) {
- if (empty($currentValue) || str_starts_with($name, $currentValue)) {
- $suggest[] = $name;
- }
- }
-
- $suggestions->suggestValues($suggest);
- }
- }
- }
}
diff --git a/src/Commands/Database/ListCommand.php b/src/Commands/Database/ListCommand.php
index 1dc134e4..ecbb36ca 100644
--- a/src/Commands/Database/ListCommand.php
+++ b/src/Commands/Database/ListCommand.php
@@ -24,7 +24,7 @@ use Symfony\Component\Yaml\Yaml;
final class ListCommand extends Command
{
- public const CHANGEABLE_COLUMNS = [
+ private const COLUMNS_CHANGEABLE = [
iFace::COLUMN_WATCHED,
iFace::COLUMN_VIA,
iFace::COLUMN_TITLE,
@@ -34,6 +34,18 @@ final class ListCommand extends Command
iFace::COLUMN_UPDATED,
];
+ private const COLUMNS_SORTABLE = [
+ iFace::COLUMN_ID,
+ iFace::COLUMN_TYPE,
+ iFace::COLUMN_UPDATED,
+ iFace::COLUMN_WATCHED,
+ iFace::COLUMN_VIA,
+ iFace::COLUMN_TITLE,
+ iFace::COLUMN_YEAR,
+ iFace::COLUMN_SEASON,
+ iFace::COLUMN_EPISODE,
+ ];
+
private PDO $pdo;
public function __construct(private StorageInterface $storage)
@@ -51,9 +63,8 @@ final class ListCommand extends Command
'via',
null,
InputOption::VALUE_REQUIRED,
- 'Limit results to this specified server. This filter is not reliable. and changes based on last server query.'
+ 'Limit results to this specified backend. This filter is not reliable. and changes based on last backend query.'
)
- ->addOption('output', null, InputOption::VALUE_REQUIRED, 'Display output as [json, yaml, table]', 'table')
->addOption(
'type',
null,
@@ -65,14 +76,17 @@ final class ListCommand extends Command
->addOption('episode', null, InputOption::VALUE_REQUIRED, 'Select episode number')
->addOption('year', null, InputOption::VALUE_REQUIRED, 'Select year.')
->addOption('id', null, InputOption::VALUE_REQUIRED, 'Select db record number')
- ->addOption('sort', null, InputOption::VALUE_REQUIRED, 'sort order by [id, updated]', 'updated')
- ->addOption('asc', null, InputOption::VALUE_NONE, 'Sort records in ascending order.')
- ->addOption('desc', null, InputOption::VALUE_NONE, 'Sort records in descending order. (Default)')
+ ->addOption(
+ 'sort',
+ null,
+ InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
+ 'Set sort by columns. for example, --sort season:asc --sort episode:desc',
+ )
->addOption(
'metadata-as',
null,
InputOption::VALUE_REQUIRED,
- 'Display metadata from this server instead of latest.'
+ 'Display metadata from this backend instead of latest.'
)
->setDescription('List Database entries.');
@@ -93,13 +107,19 @@ final class ListCommand extends Command
'metadata',
null,
InputOption::VALUE_NONE,
- 'Search in (metadata) provided by servers JSON Field using (--key, --value) options.'
+ 'Search in (metadata) provided by backends JSON Field using (--key, --value) options.'
)
->addOption(
'extra',
null,
InputOption::VALUE_NONE,
'Search in (extra information) JSON Field using (--key, --value) options.'
+ )
+ ->addOption(
+ 'dump-query',
+ null,
+ InputOption::VALUE_NONE,
+ 'Dump the generated query and exit.'
);
}
@@ -108,35 +128,35 @@ final class ListCommand extends Command
*/
protected function runCommand(InputInterface $input, OutputInterface $output): int
{
- $list = [];
-
$limit = (int)$input->getOption('limit');
+ $es = fn(string $val) => $this->storage->identifier($val);
+
$params = [
'limit' => $limit <= 0 ? 20 : $limit,
];
- $where = [];
+ $sql = $where = [];
- $sql = "SELECT * FROM state ";
+ $sql[] = sprintf('SELECT * FROM %s', $es('state'));
if ($input->getOption('id')) {
- $where[] = iFace::COLUMN_ID . ' = :id';
+ $where[] = $es(iFace::COLUMN_ID) . ' = :id';
$params['id'] = $input->getOption('id');
}
if ($input->getOption('via')) {
- $where[] = iFace::COLUMN_VIA . ' = :via';
+ $where[] = $es(iFace::COLUMN_VIA) . ' = :via';
$params['via'] = $input->getOption('via');
}
if ($input->getOption('year')) {
- $where[] = iFace::COLUMN_YEAR . ' = :year';
+ $where[] = $es(iFace::COLUMN_YEAR) . ' = :year';
$params['year'] = $input->getOption('year');
}
if ($input->getOption('type')) {
- $where[] = iFace::COLUMN_TYPE . ' = :type';
+ $where[] = $es(iFace::COLUMN_TYPE) . ' = :type';
$params['type'] = match ($input->getOption('type')) {
iFace::TYPE_MOVIE => iFace::TYPE_MOVIE,
default => iFace::TYPE_EPISODE,
@@ -144,17 +164,17 @@ final class ListCommand extends Command
}
if ($input->getOption('title')) {
- $where[] = iFace::COLUMN_TITLE . " LIKE '%' || :title || '%'";
+ $where[] = $es(iFace::COLUMN_TITLE) . ' LIKE "%" || :title || "%"';
$params['title'] = $input->getOption('title');
}
if (null !== $input->getOption('season')) {
- $where[] = iFace::COLUMN_SEASON . ' = :season';
+ $where[] = $es(iFace::COLUMN_SEASON) . ' = :season';
$params['season'] = $input->getOption('season');
}
if (null !== $input->getOption('episode')) {
- $where[] = iFace::COLUMN_EPISODE . ' = :episode';
+ $where[] = $es(iFace::COLUMN_EPISODE) . ' = :episode';
$params['episode'] = $input->getOption('episode');
}
@@ -203,20 +223,53 @@ final class ListCommand extends Command
}
if (count($where) >= 1) {
- $sql .= 'WHERE ' . implode(' AND ', $where);
+ $sql[] = 'WHERE ' . implode(' AND ', $where);
}
- $sort = match ($input->getOption('sort')) {
- 'id' => iFace::COLUMN_ID,
- 'season' => iFace::COLUMN_SEASON,
- 'episode' => iFace::COLUMN_EPISODE,
- 'type' => iFace::COLUMN_TYPE,
- default => iFace::COLUMN_UPDATED,
- };
+ $sorts = [];
- $sortOrder = ($input->getOption('asc')) ? 'ASC' : 'DESC';
+ foreach ($input->getOption('sort') as $sort) {
+ if (1 !== preg_match('/(?P\w+)(:(?P\w+))?/', $sort, $matches)) {
+ continue;
+ }
- $sql .= " ORDER BY {$sort} {$sortOrder} LIMIT :limit";
+ if (null === ($matches['field'] ?? null) || false === in_array($matches['field'], self::COLUMNS_SORTABLE)) {
+ continue;
+ }
+
+ $sorts[] = sprintf(
+ '%s %s',
+ $es($matches['field']),
+ match (strtolower($matches['dir'] ?? 'desc')) {
+ default => 'DESC',
+ 'asc' => 'ASC',
+ }
+ );
+ }
+
+ if (count($sorts) < 1) {
+ $sorts[] = sprintf('%s DESC', $es('updated'));
+ }
+
+ $sql[] = 'ORDER BY ' . implode(', ', $sorts) . ' LIMIT :limit';
+ $sql = implode(' ', $sql);
+
+ if ($input->getOption('dump-query')) {
+ $arr = [
+ 'query' => $sql,
+ 'parameters' => $params,
+ 'raw' => $this->storage->getRawSQLString($sql, $params),
+ ];
+
+ if ('table' === $input->getOption('output')) {
+ $arr['parameters'] = arrayToString($params);
+ unset($arr['raw']);
+ $arr = [$arr];
+ }
+
+ $this->displayContent($arr, $output, $input->getOption('output'));
+ return self::SUCCESS;
+ }
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
@@ -262,15 +315,16 @@ final class ListCommand extends Command
if (null !== ($via = $input->getOption('metadata-as'))) {
$path = $row[iFace::COLUMN_META_DATA][$via] ?? [];
- foreach (self::CHANGEABLE_COLUMNS as $column) {
+ foreach (self::COLUMNS_CHANGEABLE as $column) {
if (null === ($path[$column] ?? null)) {
continue;
}
$row[$column] = 'int' === get_debug_type($row[$column]) ? (int)$path[$column] : $path[$column];
}
- if (null !== ($row[iFace::COLUMN_EXTRA][$via][iFace::COLUMN_EXTRA_DATE] ?? null)) {
- $row[iFace::COLUMN_UPDATED] = $row[iFace::COLUMN_EXTRA][$via][iFace::COLUMN_EXTRA_DATE];
+
+ if (null !== ($dateFromBackend = $path[iFace::COLUMN_META_DATA_PLAYED_AT] ?? $path[iFace::COLUMN_META_DATA_ADDED_AT] ?? null)) {
+ $row[iFace::COLUMN_UPDATED] = $dateFromBackend;
}
}
@@ -280,44 +334,37 @@ final class ListCommand extends Command
unset($row);
- if ('json' === $input->getOption('output')) {
- $output->writeln(
- json_encode(
- 1 === count($rows) ? $rows[0] : $rows,
- JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
- )
- );
- } elseif ('yaml' === $input->getOption('output')) {
- $output->writeln(Yaml::dump(1 === count($rows) ? $rows[0] : $rows, 8, 2));
- } else {
- $x = 0;
+ if ('table' === $input->getOption('output')) {
+ $list = [];
foreach ($rows as $row) {
$row[iFace::COLUMN_UPDATED] = $row[iFace::COLUMN_UPDATED]->getTimestamp();
$row[iFace::COLUMN_WATCHED] = (int)$row[iFace::COLUMN_WATCHED];
$entity = Container::get(iFace::class)->fromArray($row);
- $x++;
-
- $list[] = [
- $entity->id,
- ucfirst($entity->type),
- $entity->getName(),
- $entity->via ?? '??',
- makeDate($entity->updated)->format('Y-m-d H:i:s T'),
- $entity->isWatched() ? 'Yes' : 'No',
- ag($entity->extra[$entity->via] ?? [], iFace::COLUMN_EXTRA_EVENT, '-'),
+ $item = [
+ 'id' => $entity->id,
+ 'Type' => ucfirst($entity->type),
+ 'Title' => $entity->getName(),
+ 'Via (Last)' => $entity->via ?? '??',
+ 'Date' => makeDate($entity->updated)->format('Y-m-d H:i:s T'),
+ 'Played' => $entity->isWatched() ? 'Yes' : 'No',
+ 'Via (Event)' => ag($entity->extra[$entity->via] ?? [], iFace::COLUMN_EXTRA_EVENT, '-'),
];
- if ($x < $rowCount) {
- $list[] = new TableSeparator();
- }
+ $list[] = $item;
+ $list[] = new TableSeparator();
}
$rows = null;
- (new Table($output))->setHeaders(['Id', 'Type', 'Title', 'Via (Last)', 'Date', 'Played', 'Via Event'])
- ->setStyle('box')->setRows($list)->render();
+ if (count($list) >= 2) {
+ array_pop($list);
+ }
+
+ (new Table($output))->setHeaders(array_keys($list[0] ?? []))->setStyle('box')->setRows($list)->render();
+ } else {
+ $this->displayContent($rows, $output, $input->getOption('output'));
}
return self::SUCCESS;
@@ -355,14 +402,16 @@ final class ListCommand extends Command
$suggestions->suggestValues($suggest);
}
- if ($input->mustSuggestOptionValuesFor('output')) {
+ if ($input->mustSuggestOptionValuesFor('sort')) {
$currentValue = $input->getCompletionValue();
$suggest = [];
- foreach (['json', 'yaml', 'table'] as $name) {
- if (empty($currentValue) || str_starts_with($name, $currentValue)) {
- $suggest[] = $name;
+ foreach (self::COLUMNS_SORTABLE as $name) {
+ foreach ([$name . ':desc', $name . ':asc'] as $subName) {
+ if (empty($currentValue) || true === str_starts_with($subName, $currentValue)) {
+ $suggest[] = $subName;
+ }
}
}
diff --git a/src/Commands/System/EnvCommand.php b/src/Commands/System/EnvCommand.php
index 45ea6122..9088ab35 100644
--- a/src/Commands/System/EnvCommand.php
+++ b/src/Commands/System/EnvCommand.php
@@ -5,10 +5,7 @@ declare(strict_types=1);
namespace App\Commands\System;
use App\Command;
-use Symfony\Component\Console\Completion\CompletionInput;
-use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Input\InputInterface;
-use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
final class EnvCommand extends Command
@@ -16,13 +13,6 @@ final class EnvCommand extends Command
protected function configure(): void
{
$this->setName('system:env')
- ->addOption(
- 'output',
- 'o',
- InputOption::VALUE_OPTIONAL,
- sprintf('Output mode. Can be [%s].', implode(', ', $this->outputs)),
- $this->outputs[0],
- )
->setDescription('Dump loaded environment variables.');
}
@@ -53,30 +43,4 @@ final class EnvCommand extends Command
return self::SUCCESS;
}
-
- public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
- {
- parent::complete($input, $suggestions);
-
- $methods = [
- 'output' => 'outputs',
- ];
-
- foreach ($methods as $key => $of) {
- if ($input->mustSuggestOptionValuesFor($key)) {
- $currentValue = $input->getCompletionValue();
-
- $suggest = [];
-
- foreach ($this->{$of} as $name) {
- if (empty($currentValue) || str_starts_with($name, $currentValue)) {
- $suggest[] = $name;
- }
- }
-
- $suggestions->suggestValues($suggest);
- }
- }
- }
-
}
diff --git a/src/Libs/Storage/PDO/PDOAdapter.php b/src/Libs/Storage/PDO/PDOAdapter.php
index e9d39b7d..93387372 100644
--- a/src/Libs/Storage/PDO/PDOAdapter.php
+++ b/src/Libs/Storage/PDO/PDOAdapter.php
@@ -33,8 +33,15 @@ final class PDOAdapter implements StorageInterface
'update' => null,
];
+ private string $driver = 'sqlite';
+
public function __construct(private LoggerInterface $logger, private PDO $pdo)
{
+ $driver = $this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME);
+
+ if (is_string($driver)) {
+ $this->driver = $driver;
+ }
}
public function setOptions(array $options): self
@@ -515,13 +522,45 @@ final class PDOAdapter implements StorageInterface
*/
public function getRawSQLString(string $sql, array $parameters): string
{
- $keys = $replace = [];
+ $replacer = [];
foreach ($parameters as $key => $val) {
- $keys[] = '/(\:' . preg_quote($key, '/') . ')(?:\b|\,)/';
- $replace[] = ctype_digit((string)$val) ? $val : "'{$val}'";
+ $replacer['/(\:' . preg_quote($key, '/') . ')(?:\b|\,)/'] = ctype_digit(
+ (string)$val
+ ) ? (int)$val : '"' . $val . '"';
}
- return preg_replace($keys, $replace, $sql);
+ return preg_replace(array_keys($replacer), array_values($replacer), $sql);
}
+
+ public function identifier(string $text, bool $quote = true): string
+ {
+ // table or column has to be valid ASCII name.
+ // this is opinionated, but we only allow [a-zA-Z0-9_] in column/table name.
+ if (!\preg_match('#\w#', $text)) {
+ throw new \RuntimeException(
+ sprintf(
+ 'Invalid identifier "%s": Column/table must be valid ASCII code.',
+ $text
+ )
+ );
+ }
+
+ // The first character cannot be [0-9]:
+ if (\preg_match('/^\d/', $text)) {
+ throw new \RuntimeException(
+ sprintf(
+ 'Invalid identifier "%s": Must begin with a letter or underscore.',
+ $text
+ )
+ );
+ }
+
+ return !$quote ? $text : match ($this->driver) {
+ 'mssql' => '[' . $text . ']',
+ 'mysql' => '`' . $text . '`',
+ default => '"' . $text . '"',
+ };
+ }
+
}