Compare commits
20 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
08282a7c01 | 6 months ago |
![]() |
c4a81079cc | 6 months ago |
![]() |
7fe0eea59d | 8 months ago |
![]() |
fda925927b | 9 months ago |
![]() |
2f19a89609 | 9 months ago |
![]() |
ca6db43fed | 9 months ago |
![]() |
4ce251a8bc | 9 months ago |
![]() |
0a9a99f13c | 11 months ago |
![]() |
be47153bce | 2 years ago |
![]() |
b4d3006ef9 | 2 years ago |
![]() |
f4f5f8a943 | 2 years ago |
![]() |
9440e3edb6 | 2 years ago |
![]() |
38c1a583dc | 2 years ago |
![]() |
56e05cdc2e | 2 years ago |
![]() |
c151e8038f | 2 years ago |
![]() |
47c5d1be51 | 2 years ago |
![]() |
de43de4ca0 | 2 years ago |
![]() |
01a7d7ffc4 | 2 years ago |
![]() |
f83533fc69 | 2 years ago |
![]() |
0ef982f8b7 | 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,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 (get_class($possibleCommand) == $commandClass) {
|
||||
return $possibleCommand->getName();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
<?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 Arrayy<string, mixed> The command arguments
|
||||
*/
|
||||
private Arrayy $commandArguments;
|
||||
|
||||
/**
|
||||
* @param string|Command $commandName The new command name
|
||||
* @param null|Arrayy<string, mixed> $commandArguments The command new arguments
|
||||
*/
|
||||
public function __construct ($commandName, ?Arrayy $commandArguments = null) {
|
||||
$this->setCommandName($commandName);
|
||||
$this->setCommandArguments($commandArguments ?? new Arrayy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the input for the command call
|
||||
*
|
||||
* @return InputInterface Generate the input for the command call
|
||||
*/
|
||||
public function generateInput (): InputInterface {
|
||||
return new ArrayInput(
|
||||
(clone $this->getCommandArguments())
|
||||
->prepend($this->getCommandName(), 'command')
|
||||
->toArray()
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 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 ($commandName): self {
|
||||
$this->commandName = $commandName instanceof Command
|
||||
? CliHelper::getCommandNameFromClass($commandName->getApplication(), get_class($commandName))
|
||||
: $commandName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The command arguments
|
||||
*
|
||||
* @return Arrayy<string, mixed> The command arguments
|
||||
*/
|
||||
public function getCommandArguments (): Arrayy {
|
||||
return $this->commandArguments;
|
||||
}
|
||||
/**
|
||||
* Set the command arguments
|
||||
*
|
||||
* @param Arrayy<string, mixed> $commandArguments The command new arguments
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setCommandArguments (Arrayy $commandArguments): self {
|
||||
$this->commandArguments = $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): self {
|
||||
$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,135 @@
|
||||
<?php
|
||||
|
||||
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 PhpMissingParamTypeInspection
|
||||
*/
|
||||
public function __construct (OutputInterface $output, $logger = null) {
|
||||
parent::__construct($output);
|
||||
$this->setLogger($logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getErrorOutput (): self {
|
||||
return new static(CliHelper::getErrorOutput($this->getOutput()), $this->getLogger());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 PhpMissingParamTypeInspection
|
||||
*/
|
||||
public function setLogger ($logger): self {
|
||||
if ($logger !== null && !$logger instanceof LoggerInterface) {
|
||||
throw new InvalidArgumentException('The logger must be null or a ' . LoggerInterface::class . ' instance');
|
||||
}
|
||||
|
||||
$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 ($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
|
||||
* @noinspection PhpReturnDocTypeMismatchInspection
|
||||
*/
|
||||
protected function getLoggerLevelFromVerbosity (int $verbosity) {
|
||||
return $verbosity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function write ($messages, bool $newline = false, int $options = 0) {
|
||||
$this->writeToLogger($messages, $newline, $options);
|
||||
return parent::write($messages, $newline, $options);
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function writeLn ($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 (): self {
|
||||
return new static(CliHelper::getErrorOutput($this->getOutput()));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function write ($messages, bool $newline = false, int $options = 0) {
|
||||
return $this->getOutput()->write($messages, $newline, $options);
|
||||
}
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function writeln ($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,64 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Requirements;
|
||||
|
||||
use jrosset\CliProgram\CliHelper;
|
||||
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
|
||||
*/
|
||||
public 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 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,113 @@
|
||||
<?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 ($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 ($default) {
|
||||
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::IS_READABLE) && !is_readable($default)) {
|
||||
throw new InvalidArgumentException('The default value is not readable');
|
||||
}
|
||||
if ($this->getOptions()->contains(FilesystemValidationOption::IS_WRITABLE) && !is_writable($default)) {
|
||||
throw new InvalidArgumentException('The default value is not writable');
|
||||
}
|
||||
if ($this->getOptions()->contains(FilesystemValidationOption::MUST_EXISTS) && !file_exists($default)) {
|
||||
throw new InvalidArgumentException('The default value doesn\'t exist');
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function validate ($value): bool {
|
||||
if (!is_string($value) && !$value instanceof Stringable) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($this->getOptions()->contains(FilesystemValidationOption::IS_READABLE) && !is_readable($value)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->getOptions()->contains(FilesystemValidationOption::IS_WRITABLE) && !is_writable($value)) {
|
||||
return false;
|
||||
}
|
||||
if ($this->getOptions()->contains(FilesystemValidationOption::MUST_EXISTS) && !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 ($options = null): self {
|
||||
if ($options instanceof FilesystemValidationOptionsList) {
|
||||
$this->options = $options;
|
||||
}
|
||||
elseif (is_int($options)) {
|
||||
$this->options = new FilesystemValidationOptionsList([$options]);
|
||||
}
|
||||
elseif (is_array($options)) {
|
||||
$this->options = new FilesystemValidationOptionsList($options);
|
||||
}
|
||||
elseif ($options === null) {
|
||||
$this->options = new FilesystemValidationOptionsList();
|
||||
}
|
||||
else {
|
||||
throw new InvalidArgumentException();
|
||||
}
|
||||
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 ($value): bool {
|
||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->setValue($value);
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Validation\Validators;
|
||||
|
||||
use jrosset\Collections\Collection;
|
||||
use ReflectionEnum;
|
||||
use ReflectionException;
|
||||
use UnitEnum;
|
||||
|
||||
/**
|
||||
* An argument/option value validator based on an enumeration
|
||||
*
|
||||
* @template TEnum of UnitEnum
|
||||
* @template-implements IValidator<TEnum>
|
||||
* @template-implements TInternalValueValidator<TEnum>
|
||||
*/
|
||||
class EnumValidator extends BasedValidator {
|
||||
/**
|
||||
* @var ReflectionEnum The enumeration
|
||||
*/
|
||||
private ReflectionEnum $enum;
|
||||
|
||||
/**
|
||||
* Create a validator
|
||||
*
|
||||
* @param class-string<UnitEnum> $enumClass The enumeration class
|
||||
*
|
||||
* @throws ReflectionException If the enumeration class doesn't exist
|
||||
*/
|
||||
public function __construct (string $enumClass) {
|
||||
$this->enum = new ReflectionEnum($enumClass);
|
||||
|
||||
$enumCases = new Collection();
|
||||
foreach ($this->enum->getCases() as $enumCase) {
|
||||
$enumCases->add($enumCase->getName());
|
||||
}
|
||||
|
||||
parent::__construct(new ListValidator($enumCases));
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function getValidDefault (mixed $default): string|bool|int|float|array {
|
||||
if ($default instanceof UnitEnum) {
|
||||
$default = $default->name;
|
||||
}
|
||||
return $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*
|
||||
* @throws ReflectionException
|
||||
*/
|
||||
public function getValue (): UnitEnum {
|
||||
$enumCase = $this->getInternalValidator()->getValue();
|
||||
return $this->enum->getCase($enumCase)->getValue();
|
||||
}
|
||||
}
|
@ -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, $options = null) {
|
||||
parent::__construct($options);
|
||||
$this->setExtensionsPattern($extensionsPattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getValidDefault ($default) {
|
||||
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 ($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): self {
|
||||
$this->extensionsPattern = $extensionsPattern;
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\CliProgram\Validation\Validators;
|
||||
|
||||
/**
|
||||
* Options of a filesystem based validator
|
||||
*/
|
||||
abstract class FilesystemValidationOption {
|
||||
public const MUST_EXISTS = 1;
|
||||
public const IS_READABLE = 2;
|
||||
public const IS_WRITABLE = 3;
|
||||
}
|
@ -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 'int';
|
||||
}
|
||||
}
|
@ -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,23 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\Tests\Commands;
|
||||
|
||||
use LogicException;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class FailedHello extends Hello {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function __construct () {
|
||||
parent::__construct('failedHello');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function checkRequirements (InputInterface $input, OutputInterface $output): void {
|
||||
throw new LogicException('The "foo" requirement failed');
|
||||
}
|
||||
}
|
@ -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::IS_READABLE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace jrosset\Tests;
|
||||
|
||||
enum Lang: string {
|
||||
case French = 'Bonjour';
|
||||
case English = 'Good morning';
|
||||
case American = 'Hello';
|
||||
}
|
@ -0,0 +1 @@
|
||||
This is a file
|
Loading…
Reference in New Issue