Compare commits
26 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
1d4075240e | 2 months ago |
![]() |
b3d82fff86 | 2 months ago |
![]() |
8152f9a673 | 2 months ago |
![]() |
fefb3bf3c1 | 6 months ago |
![]() |
6c31a16e80 | 6 months ago |
![]() |
8055eb9132 | 8 months ago |
![]() |
8bb34e1b6e | 9 months ago |
![]() |
69418f455f | 9 months ago |
![]() |
ab5cab163e | 9 months ago |
![]() |
438e256ca3 | 9 months ago |
![]() |
c1c36475de | 9 months ago |
![]() |
c40a14aba0 | 9 months ago |
![]() |
26815fcac3 | 9 months ago |
![]() |
7d21bb492c | 11 months ago |
![]() |
9d43547250 | 2 years ago |
![]() |
a55725f19d | 2 years ago |
![]() |
f641fb26eb | 2 years ago |
![]() |
ddb2a7d4b5 | 2 years ago |
![]() |
6643ce813c | 2 years ago |
![]() |
6258c680df | 2 years ago |
![]() |
98319dafb4 | 2 years ago |
![]() |
75a40b9c86 | 2 years ago |
![]() |
73c12fc82f | 2 years ago |
![]() |
2981e01945 | 2 years ago |
![]() |
ede43fb01c | 2 years ago |
![]() |
f69c7c8fa4 | 2 years ago |
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\AutoDiscovery;
|
||||
|
||||
use Arrayy\Collection\AbstractCollection;
|
||||
|
||||
/**
|
||||
* A list of command auto discovery spots
|
||||
*
|
||||
* @extends AbstractCollection<IAutoDiscoverySpot>
|
||||
*/
|
||||
class AutoDiscoverySpotsList extends AbstractCollection {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getType (): string {
|
||||
return IAutoDiscoverySpot::class;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\AutoPrefix;
|
||||
|
||||
use Arrayy\Collection\AbstractCollection;
|
||||
|
||||
/**
|
||||
* A list of managers of commands auto prefix
|
||||
*
|
||||
* @extends AbstractCollection<IAutoPrefixManager>
|
||||
*/
|
||||
class AutoPrefixManagersList extends AbstractCollection {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getType (): string {
|
||||
return IAutoPrefixManager::class;
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram;
|
||||
|
||||
use Attribute;
|
||||
use jrosset\CliProgram\AutoDiscovery\IAutoDiscoverySpot;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
|
||||
/**
|
||||
* Attribute to tag command
|
||||
*
|
||||
* Identical to {@see AsCommand} but allow empty command name (i.e., for {@see IAutoDiscoverySpot})
|
||||
*/
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class CliCommand {
|
||||
/**
|
||||
* @var string|null The commande name
|
||||
*/
|
||||
public readonly ?string $name;
|
||||
/**
|
||||
* @var string|null The command description
|
||||
*/
|
||||
public readonly ?string $description;
|
||||
/**
|
||||
* @var string[] The command aliases
|
||||
*/
|
||||
public readonly array $aliases;
|
||||
/**
|
||||
* @var bool Is the command hidden from command list ?
|
||||
*/
|
||||
public readonly bool $hidden;
|
||||
|
||||
/**
|
||||
* Initialization
|
||||
*
|
||||
* @param string|null $name The commande name
|
||||
* @param string|null $description The command description
|
||||
* @param string[] $aliases The command aliases
|
||||
* @param bool $hidden Is the command hidden from command list ?
|
||||
*/
|
||||
public function __construct (?string $name = null, ?string $description = null, array $aliases = [], bool $hidden = false) {
|
||||
$this->name = $name;
|
||||
$this->description = $description;
|
||||
$this->aliases = $aliases;
|
||||
$this->hidden = $hidden;
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram;
|
||||
|
||||
use jrosset\CliProgram\Output\OutputWrapper;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Output\ConsoleOutputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Helper methods
|
||||
*/
|
||||
abstract class CliHelper {
|
||||
/**
|
||||
* Get the “error” output if exists, else the output itself
|
||||
*
|
||||
* @param OutputInterface $output The output
|
||||
*
|
||||
* @return OutputInterface The “error” output if exists, else the output itself
|
||||
*/
|
||||
public static final function getErrorOutput (OutputInterface $output): OutputInterface {
|
||||
return $output instanceof ConsoleOutputInterface || $output instanceof OutputWrapper ? $output->getErrorOutput() : $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the name of a command class
|
||||
*
|
||||
* @param Application $application The application
|
||||
* @param class-string<Command> $commandClass The command class
|
||||
*
|
||||
* @return string|null The command name ; Null if not found
|
||||
*/
|
||||
public static final function getCommandNameFromClass (Application $application, string $commandClass): ?string {
|
||||
foreach ($application->all() as $possibleCommand) {
|
||||
if ($possibleCommand::class == $commandClass) {
|
||||
return $possibleCommand->getName();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\CommandCall;
|
||||
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* A list of arguments for a command call
|
||||
*/
|
||||
class CommandArgumentList {
|
||||
/**
|
||||
* @var array<string, mixed> The arguments
|
||||
*/
|
||||
protected array $arguments;
|
||||
|
||||
/**
|
||||
* Initialization
|
||||
*
|
||||
* @param Traversable|null $arguments The arguments
|
||||
*/
|
||||
public function __construct (?Traversable $arguments = null) {
|
||||
$this->arguments = [];
|
||||
if ($arguments !== null) {
|
||||
foreach ($arguments as $name => $value) {
|
||||
$this->arguments[$name] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The arguments
|
||||
*
|
||||
* @return array<string, mixed> The arguments
|
||||
*/
|
||||
public function getArguments (): array {
|
||||
return $this->arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an argument to the list
|
||||
*
|
||||
* Replace it if existing.
|
||||
*
|
||||
* @param string $name The argument name
|
||||
* @param mixed $value The argument value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addArgument (string $name, mixed $value): static {
|
||||
$this->arguments[$name] = $value;
|
||||
return $this;
|
||||
}
|
||||
/**
|
||||
* Add an option to the list
|
||||
*
|
||||
* Replace it if existing.
|
||||
*
|
||||
* @param string $name The option name (with or without the leading dashes)
|
||||
* @param mixed $value The option value
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addOption (string $name, mixed $value = true): static {
|
||||
$this->arguments[(str_starts_with($name, '-') ? '' : '--') . $name] = $value;
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\CommandCall;
|
||||
|
||||
use Arrayy\Arrayy;
|
||||
use jrosset\CliProgram\CliHelper;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\ArrayInput;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* A call to a command
|
||||
*/
|
||||
class CommandCall {
|
||||
/**
|
||||
* @var string The command name
|
||||
*/
|
||||
private string $commandName;
|
||||
/**
|
||||
* @var CommandArgumentList The command arguments
|
||||
*/
|
||||
private CommandArgumentList $commandArguments;
|
||||
|
||||
/**
|
||||
* @param string|Command $commandName The new command name
|
||||
* @param CommandArgumentList|Arrayy<string, mixed>|null $commandArguments The command new arguments
|
||||
*/
|
||||
public function __construct (string|Command $commandName, CommandArgumentList|Arrayy|null $commandArguments = null) {
|
||||
$this->setCommandName($commandName);
|
||||
$this->setCommandArguments($commandArguments ?? new CommandArgumentList());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the input for the command call
|
||||
*
|
||||
* @return InputInterface Generate the input for the command call
|
||||
*/
|
||||
public function generateInput (): InputInterface {
|
||||
return new ArrayInput(
|
||||
array_merge(
|
||||
[
|
||||
'command' => $this->getCommandName(),
|
||||
],
|
||||
$this->getCommandArguments()->getArguments()
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Run the command
|
||||
*
|
||||
* @param Application $app The application
|
||||
* @param OutputInterface $output The output
|
||||
*
|
||||
* @return int The command return code
|
||||
*
|
||||
* @throws Throwable If an error occurs
|
||||
*/
|
||||
public function run (Application $app, OutputInterface $output): int {
|
||||
return $app->doRun($this->generateInput(), $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* The command name
|
||||
*
|
||||
* @return string The command name
|
||||
*/
|
||||
public function getCommandName (): string {
|
||||
return $this->commandName;
|
||||
}
|
||||
/**
|
||||
* Set the command name
|
||||
*
|
||||
* @param string|Command $commandName The new command name
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCommandName (string|Command $commandName): self {
|
||||
$this->commandName = $commandName instanceof Command
|
||||
? CliHelper::getCommandNameFromClass($commandName->getApplication(), $commandName::class)
|
||||
: $commandName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The command arguments
|
||||
*
|
||||
* @return Arrayy<string, mixed> The command arguments
|
||||
*/
|
||||
public function getCommandArguments (): CommandArgumentList {
|
||||
return $this->commandArguments;
|
||||
}
|
||||
/**
|
||||
* Set the command arguments
|
||||
*
|
||||
* @param CommandArgumentList|Arrayy $commandArguments The command new arguments
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCommandArguments (CommandArgumentList|Arrayy $commandArguments): self {
|
||||
$this->commandArguments = $commandArguments instanceof CommandArgumentList ? $commandArguments : new CommandArgumentList($commandArguments);
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\CommandCall;
|
||||
|
||||
use Arrayy\Collection\AbstractCollection;
|
||||
|
||||
/**
|
||||
* A list of calls to a command
|
||||
*
|
||||
* @extends AbstractCollection<CommandCall>
|
||||
*/
|
||||
class CommandCallsList extends AbstractCollection {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getType (): string {
|
||||
return CommandCall::class;
|
||||
}
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Monolog;
|
||||
|
||||
use jrosset\ExtendedMonolog\ExceptionLogger;
|
||||
use jrosset\ExtendedMonolog\LogDirectoryHandler;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Implementation for an application with a {@see ConsoleOutputWithMonolog} and a {@see LogDirectoryHandler Monolog log directory} for each command
|
||||
*/
|
||||
trait TMonologApplication {
|
||||
/**
|
||||
* @var string The main log directory for Monolog: on subdirectory by command
|
||||
*/
|
||||
private string $logMainDirectory;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function getOutputInterfaceForCommand (Command $command, InputInterface $input, OutputInterface $output): OutputInterface {
|
||||
return new ConsoleOutputWithMonolog(
|
||||
$output,
|
||||
new ExceptionLogger(
|
||||
$command->getName(),
|
||||
[
|
||||
new LogDirectoryHandler(
|
||||
$this->getLogMainDirectory() . DIRECTORY_SEPARATOR
|
||||
. str_replace(':', DIRECTORY_SEPARATOR, $command->getName())
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The main log directory for Monolog: on subdirectory by command
|
||||
*
|
||||
* @return string The main log directory for Monolog: on subdirectory by command
|
||||
*/
|
||||
public function getLogMainDirectory (): string {
|
||||
return $this->logMainDirectory;
|
||||
}
|
||||
/**
|
||||
* Set the main log directory for Monolog: on subdirectory by command
|
||||
*
|
||||
* @param string $logMainDirectory The main log directory for Monolog: on subdirectory by command
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setLogMainDirectory (string $logMainDirectory): static {
|
||||
$this->logMainDirectory = $logMainDirectory;
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -1,76 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Monolog;
|
||||
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* Provide a {@see LoggerInterface Monolog Logger} for {@see OutputInterface}
|
||||
*/
|
||||
trait TOutputInterfaceWithMonolog {
|
||||
/**
|
||||
* @var LoggerInterface|null The logger
|
||||
*/
|
||||
private ?LoggerInterface $logger;
|
||||
|
||||
/**
|
||||
* The logger
|
||||
*
|
||||
* @return LoggerInterface|null The logger
|
||||
*/
|
||||
public function getLogger (): ?LoggerInterface {
|
||||
return $this->logger;
|
||||
}
|
||||
/**
|
||||
* Set the logger
|
||||
*
|
||||
* @param LoggerInterface|null $logger The logger
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setLogger (?LoggerInterface $logger): self {
|
||||
$this->logger = $logger;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function write($messages, bool $newline = false, int $options = 0): void {
|
||||
if (!is_iterable($messages)) {
|
||||
$messages = [$messages];
|
||||
}
|
||||
|
||||
if ($this->logger !== null && (($options & ConsoleOutputWithMonolog::OPTION_SKIP_MONOLOG) !== ConsoleOutputWithMonolog::OPTION_SKIP_MONOLOG)) {
|
||||
$verbosities = OutputInterface::VERBOSITY_QUIET | OutputInterface::VERBOSITY_NORMAL | OutputInterface::VERBOSITY_VERBOSE | OutputInterface::VERBOSITY_VERY_VERBOSE | OutputInterface::VERBOSITY_DEBUG;
|
||||
$verbosity = $verbosities & $options ?: OutputInterface::VERBOSITY_NORMAL;
|
||||
|
||||
if ($verbosity <= OutputInterface::VERBOSITY_QUIET) {
|
||||
$loggerLevel = Logger::ERROR;
|
||||
}
|
||||
elseif ($verbosity <= OutputInterface::VERBOSITY_NORMAL) {
|
||||
$loggerLevel = Logger::NOTICE;
|
||||
}
|
||||
elseif ($verbosity <= OutputInterface::VERBOSITY_VERBOSE) {
|
||||
$loggerLevel = Logger::INFO;
|
||||
}
|
||||
elseif ($verbosity <= OutputInterface::VERBOSITY_VERY_VERBOSE) {
|
||||
$loggerLevel = Logger::INFO;
|
||||
}
|
||||
elseif ($verbosity <= OutputInterface::VERBOSITY_DEBUG) {
|
||||
$loggerLevel = Logger::DEBUG;
|
||||
}
|
||||
else {
|
||||
$loggerLevel = Logger::NOTICE;
|
||||
}
|
||||
|
||||
foreach ($messages as $message) {
|
||||
$this->logger->log($loggerLevel, strip_tags($message));
|
||||
}
|
||||
}
|
||||
|
||||
parent::write($messages, $newline, $options);
|
||||
}
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
<?php
|
||||
/** @noinspection PhpMissingFieldTypeInspection */
|
||||
|
||||
namespace jrosset\CliProgram\Output;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use jrosset\CliProgram\CliHelper;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* A generic output interface with a {@see LoggerInterface PSR logger} connected
|
||||
*
|
||||
* @template TLogger of LoggerInterface
|
||||
*/
|
||||
class OutputWithLogger extends OutputWrapper {
|
||||
/**
|
||||
* Option to not write messages to logger
|
||||
*
|
||||
* @see static::write()
|
||||
* @see static::writeln()
|
||||
*/
|
||||
public const OPTION_SKIP_LOGGER = 4096;
|
||||
|
||||
/**
|
||||
* @var TLogger|null The logger
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* Initialization
|
||||
*
|
||||
* @param OutputInterface $output The real output interface
|
||||
* @param TLogger|null $logger The logger
|
||||
*
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function __construct (OutputInterface $output, LoggerInterface $logger = null) {
|
||||
parent::__construct($output);
|
||||
$this->setLogger($logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getErrorOutput (): static {
|
||||
return new static(CliHelper::getErrorOutput($this->getOutput()), $this->logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* The logger
|
||||
*
|
||||
* @return TLogger|null The logger
|
||||
*/
|
||||
public function getLogger () {
|
||||
return $this->logger;
|
||||
}
|
||||
/**
|
||||
* Set the logger
|
||||
*
|
||||
* @param TLogger|null $logger The logger
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException If the logger is not null or an instance for {@see LoggerInterface}
|
||||
*
|
||||
* @noinspection PhpDocSignatureInspection
|
||||
*/
|
||||
public function setLogger (LoggerInterface $logger): self {
|
||||
$this->logger = $logger;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write into the logger
|
||||
*
|
||||
* @param iterable|string $messages The messages to write
|
||||
* @param bool $newline Newline at the end ?
|
||||
* @param int $options A bitmask of options
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function writeToLogger (iterable|string $messages, bool $newline = false, int $options = 0): void {
|
||||
if ($this->logger === null || ($options & static::OPTION_SKIP_LOGGER) === static::OPTION_SKIP_LOGGER) {
|
||||
return;
|
||||
}
|
||||
|
||||
//region Calcul log level
|
||||
$verbosities = OutputInterface::VERBOSITY_QUIET
|
||||
| OutputInterface::VERBOSITY_NORMAL
|
||||
| OutputInterface::VERBOSITY_VERBOSE
|
||||
| OutputInterface::VERBOSITY_VERY_VERBOSE
|
||||
| OutputInterface::VERBOSITY_DEBUG;
|
||||
$loggerLevel = $this->getLoggerLevelFromVerbosity(
|
||||
$verbosities & $options ? : OutputInterface::VERBOSITY_NORMAL
|
||||
);
|
||||
//endregion
|
||||
|
||||
if (!is_iterable($messages)) {
|
||||
$messages = [$messages];
|
||||
}
|
||||
foreach ($messages as $message) {
|
||||
$this->getLogger()->log($loggerLevel, strip_tags($message) . ($newline ? PHP_EOL : ''));
|
||||
}
|
||||
}
|
||||
/**
|
||||
* OGet the logger level from verbosity
|
||||
*
|
||||
* @param int $verbosity The verbosity
|
||||
*
|
||||
* @return mixed The logger level
|
||||
*/
|
||||
protected function getLoggerLevelFromVerbosity (int $verbosity): mixed {
|
||||
return $verbosity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function write (iterable|string $messages, bool $newline = false, int $options = 0) {
|
||||
$this->writeToLogger($messages, $newline, $options);
|
||||
return parent::write($messages, $newline, $options);
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function writeln (iterable|string $messages, int $options = 0) {
|
||||
$this->writeToLogger($messages, true, $options);
|
||||
return parent::writeln($messages, $options);
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Output;
|
||||
|
||||
use jrosset\CliProgram\CliHelper;
|
||||
use Symfony\Component\Console\Formatter\OutputFormatterInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* A wrapper for an OutputInterface
|
||||
*/
|
||||
abstract class OutputWrapper implements OutputInterface {
|
||||
/**
|
||||
* @var OutputInterface The real output interface
|
||||
*/
|
||||
private OutputInterface $output;
|
||||
|
||||
/**
|
||||
* Initialization
|
||||
*
|
||||
* @param OutputInterface $output The real output interface
|
||||
*/
|
||||
public function __construct (OutputInterface $output) {
|
||||
$this->output = $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* The real output interface
|
||||
*
|
||||
* @return OutputInterface The real output interface
|
||||
*/
|
||||
public function getOutput (): OutputInterface {
|
||||
return $this->output;
|
||||
}
|
||||
/**
|
||||
* The “error” output
|
||||
*
|
||||
* @return static The “error” output
|
||||
*/
|
||||
public function getErrorOutput (): static {
|
||||
return new static(CliHelper::getErrorOutput($this->getOutput()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function write (iterable|string $messages, bool $newline = false, int $options = 0) {
|
||||
return $this->getOutput()->write($messages, $newline, $options);
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function writeln (iterable|string $messages, int $options = 0) {
|
||||
return $this->getOutput()->writeln($messages, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setVerbosity (int $level) {
|
||||
return $this->getOutput()->setVerbosity($level);
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getVerbosity (): int {
|
||||
return $this->getOutput()->getVerbosity();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isQuiet (): bool {
|
||||
return $this->getOutput()->isQuiet();
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isVerbose (): bool {
|
||||
return $this->getOutput()->isVerbose();
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isVeryVerbose (): bool {
|
||||
return $this->getOutput()->isVeryVerbose();
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isDebug (): bool {
|
||||
return $this->getOutput()->isDebug();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function setDecorated (bool $decorated) {
|
||||
return $this->getOutput()->setDecorated($decorated);
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function isDecorated (): bool {
|
||||
return $this->getOutput()->isDecorated();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @noinspection PhpInappropriateInheritDocUsageInspection
|
||||
*/
|
||||
public function setFormatter (OutputFormatterInterface $formatter) {
|
||||
return $this->getOutput()->setFormatter($formatter);
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getFormatter (): OutputFormatterInterface {
|
||||
return $this->getOutput()->getFormatter();
|
||||
}
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Output;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Implementation for an application with a by-command {@see OutputInterface}
|
||||
*/
|
||||
trait TCommandOutputApplication {
|
||||
/**
|
||||
* Get the {@see OutputInterface} for a command
|
||||
*
|
||||
* @param Command $command The command
|
||||
* @param InputInterface $input The input
|
||||
* @param OutputInterface $output The existing output
|
||||
*
|
||||
* @return OutputInterface The output for the command
|
||||
*
|
||||
* @throws Throwable On error
|
||||
*
|
||||
* @noinspection PhpUnusedParameterInspection
|
||||
*/
|
||||
protected function getOutputInterfaceForCommand (Command $command, InputInterface $input, OutputInterface $output): OutputInterface {
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function doRunCommand (Command $command, InputInterface $input, OutputInterface $output): int {
|
||||
return parent::doRunCommand($command, $input, $this->getOutputInterfaceForCommand($command, $input, $output));
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Requirements;
|
||||
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Interface for a command requirements
|
||||
*/
|
||||
interface IRequirements {
|
||||
/**
|
||||
* Check the requirements
|
||||
*
|
||||
* @param InputInterface $input The command input
|
||||
* @param OutputInterface $output The command output
|
||||
*
|
||||
* @return void
|
||||
*
|
||||
* @throws Throwable If a requirement failed
|
||||
*/
|
||||
public function checkRequirements (InputInterface $input, OutputInterface $output): void;
|
||||
}
|
@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Requirements;
|
||||
|
||||
use jrosset\CliProgram\CliHelper;
|
||||
use ReflectionAttribute;
|
||||
use ReflectionClass;
|
||||
use Symfony\Component\Console\ConsoleEvents;
|
||||
use Symfony\Component\Console\Event\ConsoleCommandEvent;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* An application with command requirements checking
|
||||
*
|
||||
* @see IRequirements
|
||||
*/
|
||||
trait TRequirementsApplication {
|
||||
/**
|
||||
* Register the listener for command requirements
|
||||
*
|
||||
* @param EventDispatcherInterface|null $dispatcher The event dispatcher is already existing
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected final function registerCommandRequirementsListener (?EventDispatcherInterface $dispatcher = null): void {
|
||||
$dispatcher ??= new EventDispatcher();
|
||||
$dispatcher->addListener(ConsoleEvents::COMMAND, $this->checkCommandRequirements(...));
|
||||
$this->setDispatcher($dispatcher);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the requirements of a command
|
||||
*
|
||||
* @param ConsoleCommandEvent $event The command event
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function checkCommandRequirements (ConsoleCommandEvent $event): void {
|
||||
$commandInput = $event->getInput();
|
||||
$commandOutput = $event->getOutput();
|
||||
try {
|
||||
//region Check the command is valid
|
||||
if (($command = $event->getCommand()) === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$commandReflection = new ReflectionClass($command);
|
||||
//endregion
|
||||
//region Check attribute requirements
|
||||
foreach ($commandReflection->getAttributes(IRequirements::class, ReflectionAttribute::IS_INSTANCEOF) as $commandRequirementsAttribute) {
|
||||
/** @var IRequirements $commandRequirementsAttributeInstance */
|
||||
$commandRequirementsAttributeInstance = $commandRequirementsAttribute->newInstance();
|
||||
$commandRequirementsAttributeInstance->checkRequirements($commandInput, $commandOutput);
|
||||
}
|
||||
//endregion
|
||||
//region Contrôle pré-requis (implémentation directe)
|
||||
if ($command instanceof IRequirements) {
|
||||
$command->checkRequirements($commandInput, $commandOutput);
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
catch (Throwable $exception) {
|
||||
$this->renderThrowable($exception, CliHelper::getErrorOutput($commandOutput));
|
||||
|
||||
$event->disableCommand();
|
||||
$event->stopPropagation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Validation\Validators;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* An argument/option value validator for a directory path
|
||||
*/
|
||||
class DirectoryValidator implements IValidator {
|
||||
use TInternalValueValidator;
|
||||
|
||||
/**
|
||||
* @var FilesystemValidationOptionsList The options
|
||||
*/
|
||||
private FilesystemValidationOptionsList $options;
|
||||
|
||||
/**
|
||||
* Initialization
|
||||
*
|
||||
* @param FilesystemValidationOptionsList|FilesystemValidationOption[]|FilesystemValidationOption|null $options The options
|
||||
*/
|
||||
public function __construct (FilesystemValidationOptionsList|array|FilesystemValidationOption|null $options = null) {
|
||||
$this->setOptions($options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a valid default value from the initial/given default value
|
||||
*
|
||||
* @param string|Stringable|null $default The initial/given default value
|
||||
*
|
||||
* @return string|bool|int|float|array|null The valid default value
|
||||
*
|
||||
* @throws InvalidArgumentException If the default value is not a string or null
|
||||
* @throws InvalidArgumentException If the default directory doesn't match the options
|
||||
*/
|
||||
public function getValidDefault (mixed $default): string|bool|int|float|array|null {
|
||||
if ($default === null) {
|
||||
return null;
|
||||
}
|
||||
if (!is_string($default) && !$default instanceof Stringable) {
|
||||
throw new InvalidArgumentException('The default value must be a string or null');
|
||||
}
|
||||
|
||||
if ($this->getOptions()->contains(FilesystemValidationOption::IsReadable) && !is_readable($default)) {
|
||||
throw new InvalidArgumentException('The default value is not readable');
|
||||
}
|
||||
if ($this->getOptions()->contains(FilesystemValidationOption::IsWritable) && !is_writable($default)) {
|
||||
throw new InvalidArgumentException('The default value is not writable');
|
||||
}
|
||||
if ($this->getOptions()->contains(FilesystemValidationOption::MustExists) && !file_exists($default)) {
|
||||
throw new InvalidArgumentException('The default value doesn\'t exist');
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function validate (mixed $value): bool {
|
||||
if (!is_string($value) && !$value instanceof Stringable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getOptions()->contains(FilesystemValidationOption::IsReadable->name) && !is_readable($value)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->getOptions()->contains(FilesystemValidationOption::IsWritable->name) && !is_writable($value)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->getOptions()->contains(FilesystemValidationOption::MustExists->name) && !file_exists($value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setValue($value);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The options
|
||||
*
|
||||
* @return FilesystemValidationOptionsList The options
|
||||
*/
|
||||
public function getOptions (): FilesystemValidationOptionsList {
|
||||
return $this->options;
|
||||
}
|
||||
/**
|
||||
* Replace the options
|
||||
*
|
||||
* @param FilesystemValidationOptionsList|FilesystemValidationOption[]|FilesystemValidationOption|null $options The options
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setOptions (FilesystemValidationOptionsList|array|FilesystemValidationOption|null $options = null): static {
|
||||
$this->options = match (true) {
|
||||
$options instanceof FilesystemValidationOptionsList => $options,
|
||||
$options instanceof FilesystemValidationOption => new FilesystemValidationOptionsList([$options]),
|
||||
is_array($options) => new FilesystemValidationOptionsList($options),
|
||||
$options === null => new FilesystemValidationOptionsList(),
|
||||
};
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Validation\Validators;
|
||||
|
||||
/**
|
||||
* An argument/option value validator expecting an email address
|
||||
*/
|
||||
class EmailValidator implements IValidator {
|
||||
use TInternalValueValidator;
|
||||
use TIdenticalValidDefaultValidator;
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function validate (mixed $value): bool {
|
||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setValue($value);
|
||||
return true;
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Validation\Validators;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* An argument/option value validator for a file path
|
||||
*/
|
||||
class FileValidator extends DirectoryValidator {
|
||||
/**
|
||||
* @var string|null The allowed extensions (regex pattern)
|
||||
*/
|
||||
private ?string $extensionsPattern;
|
||||
|
||||
/**
|
||||
* Initialization
|
||||
*
|
||||
* @param string|null $extensionsPattern The allowed extensions (regex pattern)
|
||||
* @param FilesystemValidationOptionsList|FilesystemValidationOption[]|FilesystemValidationOption|null $options The options
|
||||
*/
|
||||
public function __construct (?string $extensionsPattern = null, FilesystemValidationOptionsList|FilesystemValidationOption|array|null $options = null) {
|
||||
parent::__construct($options);
|
||||
$this->setExtensionsPattern($extensionsPattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getValidDefault (mixed $default): string|bool|int|float|array|null {
|
||||
if (($default = parent::getValidDefault($default)) === null) {
|
||||
return null;
|
||||
}
|
||||
if ($this->extensionsPattern !== null && preg_match($this->extensionsPattern, $default) !== 1) {
|
||||
throw new InvalidArgumentException('The default value has not a valid extension');
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function validate (mixed $value): bool {
|
||||
if (!parent::validate($value)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->extensionsPattern !== null && preg_match($this->extensionsPattern, $value) !== 1) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The allowed extensions (regex pattern)
|
||||
*
|
||||
* @return string|null The allowed extensions (regex pattern)
|
||||
*/
|
||||
public function getExtensionsPattern (): ?string {
|
||||
return $this->extensionsPattern;
|
||||
}
|
||||
/**
|
||||
* Replace the allowed extensions
|
||||
*
|
||||
* @param string|null $extensionsPattern The allowed extensions (regex pattern)
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setExtensionsPattern (?string $extensionsPattern = null): static {
|
||||
$this->extensionsPattern = $extensionsPattern;
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Validation\Validators;
|
||||
|
||||
/**
|
||||
* Options of a filesystem based validator
|
||||
*/
|
||||
enum FilesystemValidationOption {
|
||||
case MustExists;
|
||||
case IsReadable;
|
||||
case IsWritable;
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Validation\Validators;
|
||||
|
||||
use Arrayy\Collection\AbstractCollection;
|
||||
|
||||
/**
|
||||
* A list of options of a filesystem based validator
|
||||
*
|
||||
* @extends AbstractCollection<FilesystemValidationOption>
|
||||
*/
|
||||
class FilesystemValidationOptionsList extends AbstractCollection {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getType (): string {
|
||||
return FilesystemValidationOption::class;
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\Tests\Commands;
|
||||
|
||||
use jrosset\CliProgram\Validation\CommandWithValidation;
|
||||
use jrosset\CliProgram\Validation\Validators\EmailValidator;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class Email extends CommandWithValidation {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function configure () {
|
||||
parent::configure();
|
||||
|
||||
$this->addArgument(
|
||||
'email',
|
||||
InputArgument::REQUIRED,
|
||||
'The email address',
|
||||
null,
|
||||
new EmailValidator()
|
||||
);
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function execute (InputInterface $input, OutputInterface $output): int {
|
||||
$output->writeln('<info>' . $input->getArgument('email') . '</info>');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\Tests\Commands;
|
||||
|
||||
use jrosset\Tests\FailedRequirement;
|
||||
|
||||
#[FailedRequirement]
|
||||
class FailedHello extends Hello {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __construct () {
|
||||
parent::__construct('failedHello');
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\Tests\Commands;
|
||||
|
||||
use jrosset\CliProgram\Validation\CommandWithValidation;
|
||||
use jrosset\CliProgram\Validation\Validators\FilesystemValidationOption;
|
||||
use jrosset\CliProgram\Validation\Validators\FileValidator;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class ReadFile extends CommandWithValidation {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
protected static $defaultDescription = 'Read a file';
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function configure (): void {
|
||||
parent::configure();
|
||||
|
||||
$this->addArgument(
|
||||
'file',
|
||||
InputArgument::REQUIRED,
|
||||
'The file to read',
|
||||
null,
|
||||
new FileValidator(
|
||||
/** @lang PhpRegExp */ '#\.txt$#i',
|
||||
FilesystemValidationOption::IsReadable
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
protected function execute (InputInterface $input, OutputInterface $output): int {
|
||||
if (($fileContent = file_get_contents($input->getArgument('file'))) === false) {
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$output->writeln($fileContent);
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\Tests;
|
||||
|
||||
use Attribute;
|
||||
use jrosset\CliProgram\Requirements\IRequirements;
|
||||
use LogicException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class FailedRequirement implements IRequirements {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function checkRequirements (InputInterface $input, OutputInterface $output): void {
|
||||
throw new LogicException('The "foo" requirement failed');
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\Tests;
|
||||
|
||||
use Attribute;
|
||||
use jrosset\CliProgram\Requirements\IRequirements;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
class SuccessRequirement implements IRequirements {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function checkRequirements (InputInterface $input, OutputInterface $output): void {
|
||||
}
|
||||
}
|
@ -0,0 +1 @@
|
||||
This is a file
|
Loading…
Reference in New Issue