src/Commands/CycleORM/DatabaseTableCommand.php
<?php declare(strict_types=1);
/*
* This file is part of Biurad opensource projects.
*
* @copyright 2019 Biurad Group (https://biurad.com/)
* @license https://opensource.org/licenses/BSD-3-Clause License
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flange\Commands\CycleORM;
use Cycle\Database\DatabaseInterface;
use Cycle\Database\DatabaseProviderInterface;
use Cycle\Database\Driver\DriverInterface;
use Cycle\Database\Exception\DBALException;
use Cycle\Database\Injection\FragmentInterface;
use Cycle\Database\Query\QueryParameters;
use Cycle\Database\Schema\AbstractColumn;
use Cycle\Database\Schema\AbstractForeignKey;
use Cycle\Database\Schema\AbstractIndex;
use Cycle\Database\Schema\AbstractTable;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
/**
* Describes table schema for a database.
*
* @author Divine Niiquaye Ibok <divineibok@gmail.com>
*/
class DatabaseTableCommand extends Command
{
protected static $defaultName = 'cycle:database:table';
private SymfonyStyle $io;
private Table $table;
public function __construct(private DatabaseProviderInterface $provider)
{
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function configure(): void
{
$this
->setDefinition([
new InputArgument('table', InputArgument::REQUIRED, 'Table name'),
new InputOption('database', 'd', InputOption::VALUE_OPTIONAL, 'Source database', 'default'),
])
->setDescription('Describe table schema of specific database')
;
}
/**
* This optional method is the first one executed for a command after configure()
* and is useful to initialize properties based on the input arguments and options.
*/
protected function initialize(InputInterface $input, OutputInterface $output): void
{
// SymfonyStyle is an optional feature that Symfony provides so you can
// apply a consistent look to the commands of your application.
// See https://symfony.com/doc/current/console/style.html
$this->io = new SymfonyStyle($input, $output);
$this->table = new Table($output);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$database = $this->provider->database($input->getOption('database'));
$schema = $database->table($input->getArgument('table'))->getSchema();
if (!$schema->exists()) {
throw new DBALException("Table {$database->getName()}.{$input->getArgument('table')} does not exists.");
}
$output->writeln(
\sprintf(
"\n<fg=cyan>Columns of </fg=cyan><comment>%s.%s</comment>:\n",
$database->getName(),
$input->getArgument('table')
)
);
$this->describeColumns($schema);
if (!empty($indexes = $schema->getIndexes())) {
$this->describeIndexes($database, $indexes, $input);
}
if (!empty($foreignKeys = $schema->getForeignKeys())) {
$this->describeForeignKeys($database, $foreignKeys, $input);
}
$output->write("\n");
return self::SUCCESS;
}
protected function describeColumns(AbstractTable $schema): void
{
$columnsTable = $this->table->setHeaders(
[
'Column:',
'Database Type:',
'Abstract Type:',
'PHP Type:',
'Default Value:',
]
);
foreach ($schema->getColumns() as $column) {
$name = $column->getName();
if (\in_array($column->getName(), $schema->getPrimaryKeys(), true)) {
$name = "<fg=magenta>{$name}</fg=magenta>";
}
$defaultValue = $this->describeDefaultValue($column, $schema->getDriver());
$columnsTable->addRow(
[
$name,
$this->describeType($column),
$this->describeAbstractType($column),
$column->getType(),
$defaultValue ?? '<comment>---</comment>',
]
);
}
$columnsTable->render();
}
/**
* @param array<int,AbstractIndex> $indexes
*/
protected function describeIndexes(DatabaseInterface $database, array $indexes, InputInterface $input): void
{
$this->sprintf(
"\n<fg=cyan>Indexes of </fg=cyan><comment>%s.%s</comment>:\n",
$database->getName(),
$input->getArgument('table')
);
$indexesTable = $this->table->setHeaders(['Name:', 'Type:', 'Columns:']);
foreach ($indexes as $index) {
$indexesTable->addRow(
[
$index->getName(),
$index->isUnique() ? 'UNIQUE INDEX' : 'INDEX',
\implode(', ', $index->getColumns()),
]
);
}
$indexesTable->render();
}
/**
* @param array<int,AbstractForeignKey> $foreignKeys
*/
protected function describeForeignKeys(DatabaseInterface $database, array $foreignKeys, InputInterface $input): void
{
$this->sprintf(
"\n<fg=cyan>Foreign Keys of </fg=cyan><comment>%s.%s</comment>:\n",
$database->getName(),
$input->getArgument('table')
);
$foreignTable = $this->table->setHeaders(
[
'Name:',
'Column:',
'Foreign Table:',
'Foreign Column:',
'On Delete:',
'On Update:',
]
);
foreach ($foreignKeys as $reference) {
$foreignTable->addRow(
[
$reference->getName(),
\implode(', ', $reference->getColumns()),
$reference->getForeignTable(),
\implode(', ', $reference->getForeignKeys()),
$reference->getDeleteRule(),
$reference->getUpdateRule(),
]
);
}
$foreignTable->render();
}
/**
* @return mixed
*/
protected function describeDefaultValue(AbstractColumn $column, DriverInterface $driver)
{
$defaultValue = $column->getDefaultValue();
if ($defaultValue instanceof FragmentInterface) {
$value = $driver->getQueryCompiler()->compile(new QueryParameters(), '', $defaultValue);
return "<info>{$value}</info>";
}
if ($defaultValue instanceof \DateTimeInterface) {
$defaultValue = $defaultValue->format('c');
}
return $defaultValue;
}
/**
* Identical to write function but provides ability to format message. Does not add new line.
*
* @param array ...$args
*/
protected function sprintf(string $format, ...$args)
{
return $this->io->write(\sprintf($format, ...$args), false);
}
private function describeType(AbstractColumn $column): string
{
$type = $column->getType();
$abstractType = $column->getAbstractType();
if ($column->getSize()) {
$type .= " ({$column->getSize()})";
}
if ('decimal' === $abstractType) {
$type .= " ({$column->getPrecision()}, {$column->getScale()})";
}
return $type;
}
private function describeAbstractType(AbstractColumn $column): string
{
$abstractType = $column->getAbstractType();
if (\in_array($abstractType, ['primary', 'bigPrimary'], true)) {
$abstractType = "<fg=magenta>{$abstractType}</fg=magenta>";
}
return $abstractType;
}
}