Compare commits

...

20 Commits
master ... 2.x

Author SHA1 Message Date
Julien Rosset 08282a7c01 Fix command with validation when using -n/--no-interaction option 6 months ago
Julien Rosset c4a81079cc OutputWithLogger : fix method for “error” output 6 months ago
Julien Rosset 7fe0eea59d OutputWithLogger : add method for “error” output 8 months ago
Julien Rosset fda925927b Revert "CommandCall : allow command class name as command name"
This reverts commit 2f19a89609.
9 months ago
Julien Rosset 2f19a89609 CommandCall : allow command class name as command name 9 months ago
Julien Rosset ca6db43fed CommandCall: fix command name when AutoPrix or AutoDiscovery 9 months ago
Julien Rosset 4ce251a8bc Add command requirements support 9 months ago
Julien Rosset 0a9a99f13c Add method for normalizing command name in TAutoDiscoveryApplication 11 months ago
Julien Rosset be47153bce Suppression alerte exécution dans AutoDiscoveryDirectory 2 years ago
Julien Rosset b4d3006ef9 Correction respect des sauts de lignes dans les loggers 2 years ago
Julien Rosset f4f5f8a943 CommandCall: add default value for argument list 2 years ago
Julien Rosset 9440e3edb6 Add CommandCall 2 years ago
Julien Rosset 38c1a583dc Replace jrosset/collections with voku/arrayy 2 years ago
Julien Rosset 56e05cdc2e Fix arguments/options validation when "IS_ARRAY" is true 2 years ago
Julien Rosset c151e8038f Correction dépendance composer 2 years ago
Julien Rosset 47c5d1be51 Add validator for directory and file 2 years ago
Julien Rosset de43de4ca0 Generalize application for any LoggerInterface 2 years ago
Julien Rosset 01a7d7ffc4 Add EmailValidator 2 years ago
Julien Rosset f83533fc69 Validator : do not check Null or False (default value / not provided) 2 years ago
Julien Rosset 0ef982f8b7 PHP 7.4 & PHP 8.0 → branche 2.x 2 years ago

@ -9,17 +9,19 @@
}, },
"extra": { "extra": {
"branch-alias": { "branch-alias": {
"dev-master": "3.x-dev" "dev-master": "2.x-dev"
} }
}, },
"minimum-stability": "stable", "minimum-stability": "stable",
"require": { "require": {
"php": "^8.1", "php": "^7.4 || ^8.0.0",
"symfony/console": "^6.1",
"jrosset/betterphptoken": "^1.0", "jrosset/betterphptoken": "^1.0",
"jrosset/collections": "^3.0", "jrosset/extendedmonolog": "^1.1",
"jrosset/extendedmonolog": "^2.0" "psr/log": "^1.1",
"symfony/console": "^5.4",
"symfony/event-dispatcher": "^5.4",
"voku/arrayy": "^7.9"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

@ -2,21 +2,14 @@
namespace jrosset\CliProgram; namespace jrosset\CliProgram;
use jrosset\CliProgram\Monolog\ConsoleOutputWithMonolog; use jrosset\CliProgram\Monolog\TMonologApplication;
use jrosset\ExtendedMonolog\ExceptionLogger;
use jrosset\ExtendedMonolog\LogDirectoryHandler; use jrosset\ExtendedMonolog\LogDirectoryHandler;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/** /**
* An application with a {@see LogDirectoryHandler Monolog log directory} for each command * An application with a {@see LogDirectoryHandler Monolog log directory} for each command
*/ */
class ApplicationWithCommandMonolog extends ApplicationWithCommandOutputInterface { class ApplicationWithCommandMonolog extends ApplicationWithCommandOutputInterface {
/** use TMonologApplication;
* @var string The main log directory for Monolog: on subdirectory by command
*/
private string $logMainDirectory;
/** /**
* Initialization * Initialization
@ -29,44 +22,4 @@ class ApplicationWithCommandMonolog extends ApplicationWithCommandOutputInterfac
parent::__construct($name, $version); parent::__construct($name, $version);
$this->setLogMainDirectory($logMainDirectory); $this->setLogMainDirectory($logMainDirectory);
} }
/**
* @inheritDoc
*/
protected function getOutputInterfaceForCommand (Command $command, InputInterface $input, OutputInterface $output): OutputInterface {
return new ConsoleOutputWithMonolog(
new ExceptionLogger(
$command->getName(),
[
new LogDirectoryHandler(
$this->getLogMainDirectory() . DIRECTORY_SEPARATOR
. str_replace(':', DIRECTORY_SEPARATOR, $command->getName())
),
]
),
$output->getVerbosity(),
$output->isDecorated(),
$output->getFormatter()
);
}
/**
* 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;
}
} }

@ -2,38 +2,13 @@
namespace jrosset\CliProgram; namespace jrosset\CliProgram;
use jrosset\CliProgram\Output\TCommandOutputApplication;
use Symfony\Component\Console\Application; use Symfony\Component\Console\Application;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
/** /**
* An application with a by-command {@see OutputInterface} * An application with a by-command {@see OutputInterface}
*/ */
class ApplicationWithCommandOutputInterface extends Application { class ApplicationWithCommandOutputInterface extends Application {
/** use 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));
}
} }

@ -156,7 +156,12 @@ class AutoDiscoveryDirectory implements IAutoDiscoverySpot {
continue; continue;
} }
$tokens = BetterPhpToken::tokenize($buffer); /*
* Comme on ne travaille que sur une partie du code (système de buffer par 512 caractères),
* l'analyse AST peut signaler des alertes si par exemple le code coupe au milieu d'un commentaire
* → ignore donc les alertes de BetterPhpToken::tokenize pour ne pas polluer les logs
*/
$tokens = @BetterPhpToken::tokenize($buffer);
$nbTokens = count($tokens); $nbTokens = count($tokens);
for (; $currTokenGlobal < $nbTokens; $currTokenGlobal++) { for (; $currTokenGlobal < $nbTokens; $currTokenGlobal++) {
$token = $tokens[$currTokenGlobal]; $token = $tokens[$currTokenGlobal];

@ -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;
}
}

@ -2,26 +2,25 @@
namespace jrosset\CliProgram\AutoDiscovery; namespace jrosset\CliProgram\AutoDiscovery;
use jrosset\Collections\Collection; use Symfony\Component\Console\Command\Command;
use jrosset\Collections\ICollection;
/** /**
* An application with command auto discovery * An application with command auto discovery
*/ */
trait TAutoDiscoveryApplication { trait TAutoDiscoveryApplication {
/** /**
* @var ICollection<IAutoDiscoverySpot> The list of discovery spots * @var AutoDiscoverySpotsList The list of discovery spots
*/ */
private ICollection $autoDiscoverySpots; private AutoDiscoverySpotsList $autoDiscoverySpots;
/** /**
* The list of discovery spots * The list of discovery spots
* *
* @return ICollection<IAutoDiscoverySpot> The list of discovery spots * @return AutoDiscoverySpotsList The list of discovery spots
*/ */
public function getAutoDiscoverySpots (): ICollection { public function getAutoDiscoverySpots (): AutoDiscoverySpotsList {
if (!isset($this->autoDiscoverySpots)) { if (!isset($this->autoDiscoverySpots)) {
$this->autoDiscoverySpots = new Collection(); $this->autoDiscoverySpots = new AutoDiscoverySpotsList();
} }
return $this->autoDiscoverySpots; return $this->autoDiscoverySpots;
} }
@ -32,6 +31,7 @@ trait TAutoDiscoveryApplication {
* @return $this * @return $this
*/ */
public function addAutoDiscoveredCommands (): self { public function addAutoDiscoveredCommands (): self {
/** @var IAutoDiscoverySpot $autoDiscoverySpot */
foreach ($this->getAutoDiscoverySpots() as $autoDiscoverySpot) { foreach ($this->getAutoDiscoverySpots() as $autoDiscoverySpot) {
foreach ($autoDiscoverySpot->getCommands() as $command) { foreach ($autoDiscoverySpot->getCommands() as $command) {
$this->add($command); $this->add($command);
@ -39,4 +39,18 @@ trait TAutoDiscoveryApplication {
} }
return $this; return $this;
} }
/**
* Normalize command's name and aliases with auto prefixes (if any)
*
* @param Command $command The commande
*
* @return Command The command
*/
public function normalizeCommandNameAndAliases (Command $command): Command {
/** @var IAutoDiscoverySpot $autoDiscoverySpot */
foreach ($this->getAutoDiscoverySpots() as $autoDiscoverySpot) {
$command = $autoDiscoverySpot->applyAutoPrefixOnCommand($command);
}
return $command;
}
} }

@ -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;
}
}

@ -2,8 +2,6 @@
namespace jrosset\CliProgram\AutoPrefix; namespace jrosset\CliProgram\AutoPrefix;
use jrosset\Collections\Collection;
use jrosset\Collections\ICollection;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
/** /**
@ -11,18 +9,18 @@ use Symfony\Component\Console\Command\Command;
*/ */
trait TAutoPrefixManagement { trait TAutoPrefixManagement {
/** /**
* @var ICollection<IAutoPrefixManager> The lists of manager for commands auto prefix * @var AutoPrefixManagersList The lists of manager for commands auto prefix
*/ */
private ICollection $autoPrefixManagers; private AutoPrefixManagersList $autoPrefixManagers;
/** /**
* The lists of manager for commands auto prefix * The lists of manager for commands auto prefix
* *
* @return ICollection<IAutoPrefixManager> The lists of manager for commands auto prefix * @return AutoPrefixManagersList The lists of manager for commands auto prefix
*/ */
public function getAutoPrefixManagers (): ICollection { public function getAutoPrefixManagers (): AutoPrefixManagersList {
if (!isset($this->autoPrefixManagers)) { if (!isset($this->autoPrefixManagers)) {
$this->autoPrefixManagers = new Collection(); $this->autoPrefixManagers = new AutoPrefixManagersList();
} }
return $this->autoPrefixManagers; return $this->autoPrefixManagers;
} }
@ -34,19 +32,24 @@ trait TAutoPrefixManagement {
* *
* @return Command The command * @return Command The command
*/ */
protected function applyAutoPrefixOnCommand (Command $command): Command { public function applyAutoPrefixOnCommand (Command $command): Command {
/** @var IAutoPrefixManager $autoPrefixManager */
foreach ($this->getAutoPrefixManagers() as $autoPrefixManager) { foreach ($this->getAutoPrefixManagers() as $autoPrefixManager) {
if (($namesPrefix = $autoPrefixManager->getCommandPrefix($command)) !== null) { if (($namesPrefix = $autoPrefixManager->getCommandPrefix($command)) !== null) {
if (mb_strlen($namesPrefix) > 0) { if (mb_strlen($namesPrefix) > 0) {
$namesPrefix .= ':'; $namesPrefix .= ':';
} }
if (self::getNamePrefix($command->getName()) !== $namesPrefix) {
$command->setName($namesPrefix . $command->getName()); $command->setName($namesPrefix . $command->getName());
}
$aliases = $command->getAliases(); $aliases = $command->getAliases();
foreach ($aliases as &$alias) { foreach ($aliases as &$alias) {
if (self::getNamePrefix($alias) !== $namesPrefix) {
$alias = $namesPrefix . $alias; $alias = $namesPrefix . $alias;
} }
}
$command->setAliases($aliases); $command->setAliases($aliases);
break; break;
@ -54,4 +57,22 @@ trait TAutoPrefixManagement {
} }
return $command; return $command;
} }
/**
* Get the prefix from a command name or alias
*
* @param string $name The command name or alias
*
* @return string The prefix (empty if none)
*/
private static function getNamePrefix (string $name): string {
$lastPos = mb_strrpos($name, ':');
if ($lastPos === false) {
return '';
}
$prefix = mb_substr($name, 0, $lastPos);
if (mb_strlen($prefix) > 0) {
$prefix .= ':';
}
return $prefix;
}
} }

@ -2,26 +2,87 @@
namespace jrosset\CliProgram; namespace jrosset\CliProgram;
use jrosset\CliProgram\CommandCall\CommandCall;
use jrosset\CliProgram\CommandCall\CommandCallsList;
use ReflectionClass; use ReflectionClass;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
/** /**
* A basic command * A basic command
*/ */
class BaseCommand extends Command { class BaseCommand extends Command {
/**
* @return string|null The command default name
*/
public static function getDefaultName (): ?string {
$reflectionClass = new ReflectionClass(static::class);
if (($name = parent::getDefaultName()) !== null) {
return $name;
}
$classShortName = $reflectionClass->getShortName();
return mb_strtolower(mb_substr($classShortName, 0, 1)) . mb_substr($classShortName, 1);
}
/**
* @return string[] The command default aliases
*/
public static function getDefaultAliases (): array {
return [];
}
/**
* @return string|null The command default description
*/
public static function getDefaultDescription (): ?string {
return parent::getDefaultDescription();
}
/**
* @return bool Is the command hidden from command list by default ?
*/
public static function getDefaultHidden (): bool {
return false;
}
/** /**
* Initialize the command * Initialize the command
* *
* @param string|null $name The command name, Null = class name * @param string|null $name The command name ; Null if the default one (cf. {@see static::getDefaultName()})
* @param string ...$aliases The command aliases * @param string[]|string|null $aliases The command aliases ; Null if the default one (cf. {@see static::getDefaultAliases()})
* @param string|null $description The command description ; Null if the default one (cf. {@see static::getDefaultDescription()})
* @param bool|null $hidden Is the command hidden from command list ? Null if the default one (cf. {@see static::getDefaultHidden()})
*/ */
public function __construct (?string $name = null, string ...$aliases) { public function __construct (?string $name = null, $aliases = null, ?string $description = null, ?bool $hidden = null) {
if ($name === null) { parent::__construct($name);
$classShortName = (new ReflectionClass($this))->getShortName(); $this->setAliases(is_string($aliases) ? [$aliases] : $aliases ?? static::getDefaultAliases());
$name = mb_strtolower(mb_substr($classShortName, 0, 1)) . mb_substr($classShortName, 1); $this->setDescription($description ?? $this->getDescription());
$this->setHidden($hidden ?? static::getDefaultHidden());
} }
parent::__construct($name); /**
$this->setAliases($aliases); * Run a list of subcommands
*
* Stop à the first subcommand returning other than {@see Command::SUCCESS}
*
* @param CommandCallsList|CommandCall $subcommandList The subcommands to run
* @param OutputInterface $output The output
*
* @return int The last executed subcommand return code
*
* @throws Throwable If an error occurs
*/
public function runSubCommands ($subcommandList, OutputInterface $output): int {
if (!$subcommandList instanceof CommandCallsList) {
$subcommandList = new CommandCallsList([$subcommandList]);
}
/** @var CommandCall $subcommand */
foreach ($subcommandList as $subcommand) {
if (($returnCode = $subcommand->run($this->getApplication(), $output)) !== Command::SUCCESS) {
return $returnCode;
}
}
return Command::SUCCESS;
} }
} }

@ -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;
}
}

@ -2,29 +2,38 @@
namespace jrosset\CliProgram\Monolog; namespace jrosset\CliProgram\Monolog;
use jrosset\CliProgram\Output\OutputWithLogger;
use Monolog\Logger;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Formatter\OutputFormatterInterface; use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\ConsoleOutput;
/** /**
* A {@see ConsoleOutput} with a {@see LoggerInterface Monolog Logger} * A generic output interface with a {@see LoggerInterface Monolog Logger}
*
* @implements OutputWithLogger<Logger>
*/ */
class ConsoleOutputWithMonolog extends ConsoleOutput { class ConsoleOutputWithMonolog extends OutputWithLogger {
use TOutputInterfaceWithMonolog;
/** /**
* Option pour ne pas écrire un message dans Monolog * @inheritDoc
*/ */
public const OPTION_SKIP_MONOLOG = 4096; protected function getLoggerLevelFromVerbosity (int $verbosity): int {
if ($verbosity <= OutputInterface::VERBOSITY_QUIET) {
/** return Logger::ERROR;
* @param LoggerInterface $logger The Monolog Logger }
* @param int $verbosity The verbosity level (one of the VERBOSITY constants in OutputInterface) elseif ($verbosity <= OutputInterface::VERBOSITY_NORMAL) {
* @param bool|null $decorated Whether to decorate messages (null for auto-guessing) return Logger::NOTICE;
* @param OutputFormatterInterface|null $formatter Output formatter instance (null to use default OutputFormatter) }
*/ elseif ($verbosity <= OutputInterface::VERBOSITY_VERBOSE) {
public function __construct (LoggerInterface $logger, int $verbosity = self::VERBOSITY_NORMAL, bool $decorated = null, OutputFormatterInterface $formatter = null) { return Logger::INFO;
parent::__construct($verbosity, $decorated, $formatter); }
$this->setLogger($logger); elseif ($verbosity <= OutputInterface::VERBOSITY_VERY_VERBOSE) {
return Logger::INFO;
}
elseif ($verbosity <= OutputInterface::VERBOSITY_DEBUG) {
return Logger::DEBUG;
}
else {
return Logger::NOTICE;
}
} }
} }

@ -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;
}
}
}

@ -5,6 +5,7 @@ namespace jrosset\CliProgram\Validation;
use Closure; use Closure;
use jrosset\CliProgram\Validation\Validators\InvalidValueException; use jrosset\CliProgram\Validation\Validators\InvalidValueException;
use jrosset\CliProgram\Validation\Validators\IValidator; use jrosset\CliProgram\Validation\Validators\IValidator;
use ReflectionFunction;
use Stringable; use Stringable;
use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
@ -25,16 +26,59 @@ trait TCommandWithValidation {
* @var array<string, IValidator> The validators of each {@see InputOption} * @var array<string, IValidator> The validators of each {@see InputOption}
*/ */
private array $optionsValidator = []; private array $optionsValidator = [];
/**
* @var Closure|null The <i>real</i> code ({@see Command::$code} is used to perform input validation before execution) to execute when running this command.
* <br><br>If set, it overrides the code defined in the execute() method.
*/
private ?Closure $realCode = null;
/** /**
* @inheritDoc * @inheritDoc
* *
* @throws Throwable If an argument or option is not valid * @throws Throwable If an argument or option is not valid
*/ */
protected function interact (InputInterface $input, OutputInterface $output): void { protected function initialize (InputInterface $input, OutputInterface $output): void {
parent::interact($input, $output); parent::setCode(
function (InputInterface $input, OutputInterface $output): int {
$this->validate($input, $output); $this->validate($input, $output);
return $this->realCode !== null
? ($this->realCode)($input, $output)
: $this->execute($input, $output);
}
);
parent::initialize($input, $output);
}
/**
* @inheritDoc
*
* @noinspection PhpMissingReturnTypeInspection
*/
public function setCode (callable $code) {
if ($code instanceof Closure) {
/** @noinspection PhpUnhandledExceptionInspection */
$codeReflection = new ReflectionFunction($code);
if ($codeReflection->getClosureThis() === null) {
set_error_handler(
static function () {
} }
);
try {
if ($rebindCode = Closure::bind($code, $this)) {
$code = $rebindCode;
}
}
finally {
restore_error_handler();
}
}
}
// {@see Command::$code} is used to perform input validation before execution, so set {@see self::$realCode} instead
$this->realCode = $code;
return $this;
}
/** /**
* Validate the command input * Validate the command input
* *
@ -59,12 +103,39 @@ trait TCommandWithValidation {
*/ */
protected function validateArguments (InputInterface $input, OutputInterface $output): void { protected function validateArguments (InputInterface $input, OutputInterface $output): void {
foreach ($this->argumentsValidator as $argumentName => $argumentValidator) { foreach ($this->argumentsValidator as $argumentName => $argumentValidator) {
if ($input->hasArgument($argumentName)) { //region Check the argument still exists
if (!$argumentValidator->validate($input->getArgument($argumentName))) { if (!$input->hasArgument($argumentName)) {
continue;
}
//endregion
//region Get the argument value
$argumentValue = $input->getArgument($argumentName);
//endregion
//region Check the argument value is not Null
if ($argumentValue === null) {
// If the value is strictly Null (the default value), don't check it → skip
continue;
}
//endregion
//region Check the value is valid and replace it by the one extracted from the validator
if ($this->getDefinition()->getArgument($argumentName)->isArray() && is_array($argumentValue)) {
// This is an “array” argument → check and replace each value instead of the array itself
foreach ($argumentValue as &$argumentValueEntry) {
if (!$argumentValidator->validate($argumentValueEntry)) {
throw new InvalidValueException(sprintf('The "%s" argument has not a valid value', $argumentName));
}
$argumentValueEntry = $argumentValidator->getValue();
}
$input->setArgument($argumentName, $argumentValue);
}
else {
if (!$argumentValidator->validate($argumentValue)) {
throw new InvalidValueException(sprintf('The "%s" argument has not a valid value', $argumentName)); throw new InvalidValueException(sprintf('The "%s" argument has not a valid value', $argumentName));
} }
$input->setArgument($argumentName, $argumentValidator->getValue()); $input->setArgument($argumentName, $argumentValidator->getValue());
} }
//endregion
} }
} }
/** /**
@ -79,12 +150,39 @@ trait TCommandWithValidation {
*/ */
protected function validateOptions (InputInterface $input, OutputInterface $output): void { protected function validateOptions (InputInterface $input, OutputInterface $output): void {
foreach ($this->optionsValidator as $optionName => $optionValidator) { foreach ($this->optionsValidator as $optionName => $optionValidator) {
if ($input->hasOption($optionName)) { //region Check the option still exists
if (!$optionValidator->validate($input->getOption($optionName))) { if (!$input->hasOption($optionName)) {
continue;
}
//endregion
//region Get and check the option value
$optionValue = $input->getOption($optionName);
//endregion
//region Check the option value is NOT Null or False
if ($optionValue === null || $optionValue === false) {
// If the value is strictly Null (the default value or not provided) or False (the default value), don't check it → skip
continue;
}
//endregion
//region Check the value is valid and replace it by the one extracted from the validator
if ($this->getDefinition()->getOption($optionName)->isArray() && is_array($optionValue)) {
// This is an “array” argument → check and replace each value instead of the array itself
foreach ($optionValue as &$optionValueEntry) {
if (!$optionValidator->validate($optionValueEntry)) {
throw new InvalidValueException(sprintf('The "--%s" option has not a valid value', $optionName));
}
$optionValueEntry = $optionValidator->getValue();
}
$input->setArgument($optionName, $optionValue);
}
else {
if (!$optionValidator->validate($optionValue)) {
throw new InvalidValueException(sprintf('The "--%s" option has not a valid value', $optionName)); throw new InvalidValueException(sprintf('The "--%s" option has not a valid value', $optionName));
} }
$input->setOption($optionName, $optionValidator->getValue()); $input->setOption($optionName, $optionValidator->getValue());
} }
//endregion
} }
} }
@ -96,7 +194,6 @@ trait TCommandWithValidation {
* @param mixed $default The default value (for InputArgument::OPTIONAL mode only) * @param mixed $default The default value (for InputArgument::OPTIONAL mode only)
* @param string $description The argument description * @param string $description The argument description
* @param IValidator|null $validator The validator or Null if none * @param IValidator|null $validator The validator or Null if none
* @param Closure|array|null $suggestedValues The function or list of suggested values when completing ; Null if none
* *
* @return $this * @return $this
* *
@ -106,21 +203,15 @@ trait TCommandWithValidation {
string $name, string $name,
int $mode = null, int $mode = null,
string $description = '', string $description = '',
mixed $default = null, $default = null,
?IValidator $validator = null, ?IValidator $validator = null
Closure|array|null $suggestedValues = null ): self {
): static {
$default = static::treatStringableDefaultValue($default); $default = static::treatStringableDefaultValue($default);
if ($validator !== null) { if ($validator !== null) {
$default = $validator->getValidDefault($default); $default = $validator->getValidDefault($default);
} }
if ($suggestedValues !== null) {
parent::addArgument($name, $mode, $description, $default, $suggestedValues);
}
else {
parent::addArgument($name, $mode, $description, $default); parent::addArgument($name, $mode, $description, $default);
}
return $this->setArgumentValidator($name, $validator); return $this->setArgumentValidator($name, $validator);
} }
/** /**
@ -133,7 +224,7 @@ trait TCommandWithValidation {
* *
* @throws InvalidArgumentException If the {@see InputArgument} doesn't exist * @throws InvalidArgumentException If the {@see InputArgument} doesn't exist
*/ */
public function setArgumentValidator (string $name, ?IValidator $validator): static { public function setArgumentValidator (string $name, ?IValidator $validator): self {
if (!$this->getDefinition()->hasArgument($name)) { if (!$this->getDefinition()->hasArgument($name)) {
throw new InvalidArgumentException(sprintf('The "%s" argument does not exist', $name)); throw new InvalidArgumentException(sprintf('The "%s" argument does not exist', $name));
} }
@ -169,7 +260,6 @@ trait TCommandWithValidation {
* @param string $description The argument description * @param string $description The argument description
* @param mixed $default The default value (must be null for InputOption::VALUE_NONE) * @param mixed $default The default value (must be null for InputOption::VALUE_NONE)
* @param IValidator|null $validator The validator or Null if none ; ignored if **$mode** = {@see InputOption::VALUE_NONE} * @param IValidator|null $validator The validator or Null if none ; ignored if **$mode** = {@see InputOption::VALUE_NONE}
* @param Closure|array|null $suggestedValues The function or list of suggested values when completing ; Null if none
* *
* @return $this * @return $this
* *
@ -177,24 +267,18 @@ trait TCommandWithValidation {
*/ */
public function addOption ( public function addOption (
string $name, string $name,
string|array|null $shortcut = null, $shortcut = null,
int $mode = null, int $mode = null,
string $description = '', string $description = '',
mixed $default = null, $default = null,
?IValidator $validator = null, ?IValidator $validator = null
Closure|array|null $suggestedValues = null ): self {
): static {
$default = static::treatStringableDefaultValue($default); $default = static::treatStringableDefaultValue($default);
if ($validator !== null) { if ($validator !== null) {
$default = $validator->getValidDefault($default); $default = $validator->getValidDefault($default);
} }
if ($suggestedValues !== null) {
parent::addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
}
else {
parent::addOption($name, $shortcut, $mode, $description, $default); parent::addOption($name, $shortcut, $mode, $description, $default);
}
return $this->setOptionValidator($name, $validator); return $this->setOptionValidator($name, $validator);
} }
/** /**
@ -207,7 +291,7 @@ trait TCommandWithValidation {
* *
* @throws InvalidArgumentException If the {@see InputOption} doesn't exist * @throws InvalidArgumentException If the {@see InputOption} doesn't exist
*/ */
public function setOptionValidator (string $name, ?IValidator $validator): static { public function setOptionValidator (string $name, ?IValidator $validator): self {
if (!$this->getDefinition()->hasOption($name)) { if (!$this->getDefinition()->hasOption($name)) {
throw new InvalidArgumentException(sprintf('The "--%s" option does not exist', $name)); throw new InvalidArgumentException(sprintf('The "--%s" option does not exist', $name));
} }
@ -241,7 +325,7 @@ trait TCommandWithValidation {
* *
* @return mixed The default value * @return mixed The default value
*/ */
protected static function treatStringableDefaultValue (mixed $default): mixed { protected static function treatStringableDefaultValue ($default) {
if ($default instanceof Stringable) { if ($default instanceof Stringable) {
return $default->__toString(); return $default->__toString();
} }

@ -20,7 +20,7 @@ abstract class AbstractDateTimeValidator extends BasedValidator {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function validate (mixed $value): bool { public function validate ($value): bool {
$this->getInternalValidator()->setPattern($this->getPattern()); $this->getInternalValidator()->setPattern($this->getPattern());
return parent::validate($value); return parent::validate($value);
} }

@ -17,6 +17,8 @@ abstract class BasedValidator implements IValidator {
* Create a validator * Create a validator
* *
* @param TValidator $internalValidator The internal validator * @param TValidator $internalValidator The internal validator
*
* @noinspection PhpMissingParamTypeInspection
*/ */
public function __construct ($internalValidator) { public function __construct ($internalValidator) {
$this->internalValidator = $internalValidator; $this->internalValidator = $internalValidator;
@ -33,7 +35,7 @@ abstract class BasedValidator implements IValidator {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function validate (mixed $value): bool { public function validate ($value): bool {
return $this->internalValidator->validate($value); return $this->internalValidator->validate($value);
} }
} }

@ -29,7 +29,7 @@ class DateTimeValidator extends AbstractDateTimeValidator {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function getValidDefault (mixed $default): string|bool|int|float|array { public function getValidDefault ($default) {
if ($default instanceof DateTimeInterface) { if ($default instanceof DateTimeInterface) {
return $default->format('Y-m-d H:i:s'); return $default->format('Y-m-d H:i:s');
} }

@ -29,7 +29,7 @@ class DateValidator extends AbstractDateTimeValidator {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function getValidDefault (mixed $default): string|bool|int|float|array { public function getValidDefault ($default) {
if ($default instanceof DateTimeInterface) { if ($default instanceof DateTimeInterface) {
return $default->format('Y-m-d'); return $default->format('Y-m-d');
} }

@ -56,7 +56,7 @@ class DecimalValidator extends BasedValidator {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function validate (mixed $value): bool { public function validate ($value): bool {
if (!parent::validate($value)) { if (!parent::validate($value)) {
return false; return false;
} }

@ -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';
}
}

@ -15,7 +15,7 @@ interface IValidator {
* *
* @return string|bool|int|float|array The valid default value * @return string|bool|int|float|array The valid default value
*/ */
public function getValidDefault (mixed $default): string|bool|int|float|array; public function getValidDefault ($default);
/** /**
* Validate a value * Validate a value
@ -24,7 +24,7 @@ interface IValidator {
* *
* @return bool True if the value is valid, else False * @return bool True if the value is valid, else False
*/ */
public function validate (mixed $value): bool; public function validate ($value): bool;
/** /**
* Get the value, after it's validation * Get the value, after it's validation
* *

@ -50,7 +50,7 @@ class IntegerValidator extends BasedValidator {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function validate (mixed $value): bool { public function validate ($value): bool {
if (!parent::validate($value)) { if (!parent::validate($value)) {
return false; return false;
} }

@ -2,8 +2,7 @@
namespace jrosset\CliProgram\Validation\Validators; namespace jrosset\CliProgram\Validation\Validators;
use jrosset\Collections\Collection; use Arrayy\Type\StringCollection;
use jrosset\Collections\ICollection;
/** /**
* An argument/option value validator based on a list of value * An argument/option value validator based on a list of value
@ -17,23 +16,23 @@ class ListValidator implements IValidator {
use TInternalValueValidator; use TInternalValueValidator;
/** /**
* @var ICollection The list of allowed values * @var StringCollection The list of allowed values
*/ */
private ICollection $allowedValues; private StringCollection $allowedValues;
/** /**
* Create a validator * Create a validator
* *
* @param ICollection|null $allowedValues The list of allowed values * @param StringCollection|null $allowedValues The list of allowed values
*/ */
public function __construct (?ICollection $allowedValues = null) { public function __construct (?StringCollection $allowedValues = null) {
$this->setAllowedValues($allowedValues ?? new Collection()); $this->setAllowedValues($allowedValues ?? new StringCollection());
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function validate (mixed $value): bool { public function validate ($value): bool {
if ($this->getAllowedValues()->contains($value)) { if ($this->getAllowedValues()->contains($value)) {
$this->setValue($value); $this->setValue($value);
return true; return true;
@ -44,19 +43,19 @@ class ListValidator implements IValidator {
/** /**
* The list of allowed values * The list of allowed values
* *
* @return ICollection The list of allowed values * @return StringCollection The list of allowed values
*/ */
public function getAllowedValues (): ICollection { public function getAllowedValues (): StringCollection {
return $this->allowedValues; return $this->allowedValues;
} }
/** /**
* Set the list of allowed values * Set the list of allowed values
* *
* @param ICollection $allowedValues The list of allowed values * @param StringCollection $allowedValues The list of allowed values
* *
* @return $this * @return $this
*/ */
public function setAllowedValues (ICollection $allowedValues): self { public function setAllowedValues (StringCollection $allowedValues): self {
$this->allowedValues = $allowedValues; $this->allowedValues = $allowedValues;
return $this; return $this;
} }

@ -32,7 +32,7 @@ class RegexValidator implements IValidator {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function validate (mixed $value): bool { public function validate ($value): bool {
return preg_match($this->pattern, $value, $this->matches) === 1; return preg_match($this->pattern, $value, $this->matches) === 1;
} }
@ -71,7 +71,7 @@ class RegexValidator implements IValidator {
* *
* @return string|null The capturing group or Null if not set * @return string|null The capturing group or Null if not set
*/ */
public function getMatch (int|string $group): ?string { public function getMatch ($group): ?string {
return $this->getMatches()[$group] ?? null; return $this->getMatches()[$group] ?? null;
} }
/** /**
@ -81,7 +81,7 @@ class RegexValidator implements IValidator {
* *
* @return bool True if the capturing group exists and is not null * @return bool True if the capturing group exists and is not null
*/ */
public function hasMatch (int|string $group): bool { public function hasMatch ($group): bool {
return $this->getMatches()[$group] !== null; return $this->getMatches()[$group] !== null;
} }

@ -11,7 +11,7 @@ trait TIdenticalValidDefaultValidator {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function getValidDefault (mixed $default): string|bool|int|float|array { public function getValidDefault ($default) {
return $default; return $default;
} }
} }

@ -32,7 +32,7 @@ trait TInternalValueValidator {
* *
* @return $this * @return $this
*/ */
protected function setValue ($value): static { protected function setValue ($value): self {
$this->value = $value; $this->value = $value;
return $this; return $this;
} }

@ -29,7 +29,7 @@ class TimeValidator extends AbstractDateTimeValidator {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function getValidDefault (mixed $default): string|bool|int|float|array { public function getValidDefault ($default) {
if ($default instanceof DateTimeInterface) { if ($default instanceof DateTimeInterface) {
return $default->format('H:i:s'); return $default->format('H:i:s');
} }

@ -6,12 +6,15 @@ use jrosset\CliProgram\ApplicationWithCommandMonolog;
use jrosset\CliProgram\AutoDiscovery\AutoDiscoveryDirectory; use jrosset\CliProgram\AutoDiscovery\AutoDiscoveryDirectory;
use jrosset\CliProgram\AutoDiscovery\TAutoDiscoveryApplication; use jrosset\CliProgram\AutoDiscovery\TAutoDiscoveryApplication;
use jrosset\CliProgram\AutoPrefix\AutoPrefixNamespaceManager; use jrosset\CliProgram\AutoPrefix\AutoPrefixNamespaceManager;
use jrosset\CliProgram\Requirements\TRequirementsApplication;
class Application extends ApplicationWithCommandMonolog { class Application extends ApplicationWithCommandMonolog {
use TAutoDiscoveryApplication; use TAutoDiscoveryApplication;
use TRequirementsApplication;
public function __construct (string $name = 'UNKNOWN', string $version = 'UNKNOWN') { public function __construct (string $name = 'UNKNOWN', string $version = 'UNKNOWN') {
parent::__construct(__DIR__ . '/logs/', $name, $version); parent::__construct(__DIR__ . '/logs/', $name, $version);
$this->registerCommandRequirementsListener();
$spot = new AutoDiscoveryDirectory(__DIR__ . '/Commands'); $spot = new AutoDiscoveryDirectory(__DIR__ . '/Commands');
$spot->getAutoPrefixManagers()->prepend(new AutoPrefixNamespaceManager('\\jrosset\\Tests\\Commands', 'test')); $spot->getAutoPrefixManagers()->prepend(new AutoPrefixNamespaceManager('\\jrosset\\Tests\\Commands', 'test'));

@ -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');
}
}

@ -4,18 +4,18 @@ namespace jrosset\Tests\Commands;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeInterface; use DateTimeInterface;
use jrosset\CliProgram\Monolog\ConsoleOutputWithMonolog; use jrosset\CliProgram\Output\OutputWithLogger;
use jrosset\CliProgram\Requirements\IRequirements;
use jrosset\CliProgram\Validation\CommandWithValidation; use jrosset\CliProgram\Validation\CommandWithValidation;
use jrosset\CliProgram\Validation\Validators\DateValidator; use jrosset\CliProgram\Validation\Validators\DateValidator;
use jrosset\CliProgram\Validation\Validators\EnumValidator;
use jrosset\CliProgram\Validation\Validators\IntegerValidator; use jrosset\CliProgram\Validation\Validators\IntegerValidator;
use jrosset\Tests\Lang;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
class Hello extends CommandWithValidation { class Hello extends CommandWithValidation implements IRequirements {
/** /**
* @inheritdoc * @inheritdoc
*/ */
@ -24,14 +24,20 @@ class Hello extends CommandWithValidation {
/** /**
* @inheritDoc * @inheritDoc
*/ */
public function __construct () { public function __construct (?string $name = null) {
parent::__construct('hello', 'bonjour'); parent::__construct($name ?? 'hello', 'bonjour');
} }
/** /**
* @inheritDoc * @inheritDoc
*/ */
protected function configure () { public function checkRequirements (InputInterface $input, OutputInterface $output): void {
}
/**
* @inheritDoc
*/
protected function configure (): void {
parent::configure(); parent::configure();
$this->addArgument( $this->addArgument(
@ -42,18 +48,10 @@ class Hello extends CommandWithValidation {
new DateValidator() new DateValidator()
); );
$this->addOption(
'lang',
'l',
InputArgument::OPTIONAL,
'The lang',
Lang::English,
new EnumValidator(Lang::class)
);
$this->addOption( $this->addOption(
'repeat', 'repeat',
'r', 'r',
InputArgument::OPTIONAL, InputOption::VALUE_OPTIONAL,
'The number of repeat', 'The number of repeat',
1, 1,
new IntegerValidator(1, null) new IntegerValidator(1, null)
@ -64,9 +62,9 @@ class Hello extends CommandWithValidation {
* @inheritDoc * @inheritDoc
*/ */
protected function execute (InputInterface $input, OutputInterface $output): int { protected function execute (InputInterface $input, OutputInterface $output): int {
$output->writeln('Command : ' . __CLASS__, OutputInterface::VERBOSITY_DEBUG); $output->writeln('<info>Command : ' . __CLASS__ . '</info>', OutputInterface::VERBOSITY_DEBUG);
$text = $input->getOption('lang')->value; $text = 'Hello';
$repeat = $input->getOption('repeat'); $repeat = $input->getOption('repeat');
/** @var DateTimeInterface $day */ /** @var DateTimeInterface $day */
@ -78,7 +76,7 @@ class Hello extends CommandWithValidation {
for ($curr = 0; $curr < $repeat; $curr++) { for ($curr = 0; $curr < $repeat; $curr++) {
$output->writeln($text); $output->writeln($text);
} }
$output->writeln('FIN', ConsoleOutputWithMonolog::OPTION_SKIP_MONOLOG); $output->writeln('FIN', OutputWithLogger::OPTION_SKIP_LOGGER);
return Command::SUCCESS; return Command::SUCCESS;
} }
} }

@ -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…
Cancel
Save