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,18 +9,18 @@
},
"extra": {
"branch-alias": {
"dev-master": "3.x-dev"
"dev-master": "2.x-dev"
}
},
"minimum-stability": "stable",
"require": {
"php": "^8.1",
"php": "^7.4 || ^8.0.0",
"jrosset/betterphptoken": "^1.0",
"jrosset/extendedmonolog": "^2.0",
"psr/log": "^2.0",
"symfony/console": "^6.1",
"symfony/event-dispatcher": "^6.1",
"jrosset/extendedmonolog": "^1.1",
"psr/log": "^1.1",
"symfony/console": "^5.4",
"symfony/event-dispatcher": "^5.4",
"voku/arrayy": "^7.9"
},
"autoload": {

@ -127,7 +127,7 @@ class AutoDiscoveryDirectory implements IAutoDiscoverySpot {
$classes[$fileInfo->getRealPath()] = $this->applyAutoPrefixOnCommand($class->newInstance());
}
catch (ReflectionException) {
catch (ReflectionException $exception) {
continue;
}
}

@ -30,7 +30,7 @@ trait TAutoDiscoveryApplication {
*
* @return $this
*/
public function addAutoDiscoveredCommands (): static {
public function addAutoDiscoveredCommands (): self {
/** @var IAutoDiscoverySpot $autoDiscoverySpot */
foreach ($this->getAutoDiscoverySpots() as $autoDiscoverySpot) {
foreach ($autoDiscoverySpot->getCommands() as $command) {

@ -18,13 +18,6 @@ class BaseCommand extends Command {
*/
public static function getDefaultName (): ?string {
$reflectionClass = new ReflectionClass(static::class);
if ($attribute = $reflectionClass->getAttributes(CliCommand::class)) {
/** @var CliCommand $cliCommandAttribute */
$cliCommandAttribute = $attribute[0]->newInstance();
if ($cliCommandAttribute->name !== null) {
return $cliCommandAttribute->name;
}
}
if (($name = parent::getDefaultName()) !== null) {
return $name;
}
@ -36,34 +29,18 @@ class BaseCommand extends Command {
* @return string[] The command default aliases
*/
public static function getDefaultAliases (): array {
$reflectionClass = new ReflectionClass(static::class);
if ($attribute = $reflectionClass->getAttributes(CliCommand::class)) {
/** @var CliCommand $cliCommandAttribute */
$cliCommandAttribute = $attribute[0]->newInstance();
return $cliCommandAttribute->aliases;
}
return [];
}
/**
* @return string|null The command default description
*/
public static function getDefaultDescription (): ?string {
if ($attribute = (new ReflectionClass(static::class))->getAttributes(CliCommand::class)) {
/** @var CliCommand $cliCommandAttribute */
$cliCommandAttribute = $attribute[0]->newInstance();
return $cliCommandAttribute->description;
}
return parent::getDefaultDescription();
}
/**
* @return bool Is the command hidden from command list by default ?
*/
public static function getDefaultHidden (): bool {
if ($attribute = (new ReflectionClass(static::class))->getAttributes(CliCommand::class)) {
/** @var CliCommand $cliCommandAttribute */
$cliCommandAttribute = $attribute[0]->newInstance();
return $cliCommandAttribute->hidden;
}
return false;
}
@ -75,7 +52,7 @@ class BaseCommand extends Command {
* @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, array|string|null $aliases = null, ?string $description = null, ?bool $hidden = null) {
public function __construct (?string $name = null, $aliases = null, ?string $description = null, ?bool $hidden = null) {
parent::__construct($name);
$this->setAliases(is_string($aliases) ? [$aliases] : $aliases ?? static::getDefaultAliases());
$this->setDescription($description ?? $this->getDescription());
@ -94,7 +71,7 @@ class BaseCommand extends Command {
*
* @throws Throwable If an error occurs
*/
public function runSubCommands (CommandCallsList|CommandCall $subcommandList, OutputInterface $output): int {
public function runSubCommands ($subcommandList, OutputInterface $output): int {
if (!$subcommandList instanceof CommandCallsList) {
$subcommandList = new CommandCallsList([$subcommandList]);
}

@ -1,47 +0,0 @@
<?php
namespace jrosset\CliProgram;
use Attribute;
use jrosset\CliProgram\AutoDiscovery\IAutoDiscoverySpot;
use Symfony\Component\Console\Attribute\AsCommand;
/**
* Attribute to tag command
*
* Identical to {@see AsCommand} but allow empty command name (i.e., for {@see IAutoDiscoverySpot})
*/
#[Attribute(Attribute::TARGET_CLASS)]
class CliCommand {
/**
* @var string|null The commande name
*/
public readonly ?string $name;
/**
* @var string|null The command description
*/
public readonly ?string $description;
/**
* @var string[] The command aliases
*/
public readonly array $aliases;
/**
* @var bool Is the command hidden from command list ?
*/
public readonly bool $hidden;
/**
* Initialization
*
* @param string|null $name The commande name
* @param string|null $description The command description
* @param string[] $aliases The command aliases
* @param bool $hidden Is the command hidden from command list ?
*/
public function __construct (?string $name = null, ?string $description = null, array $aliases = [], bool $hidden = false) {
$this->name = $name;
$this->description = $description;
$this->aliases = $aliases;
$this->hidden = $hidden;
}
}

@ -33,7 +33,7 @@ abstract class CliHelper {
*/
public static final function getCommandNameFromClass (Application $application, string $commandClass): ?string {
foreach ($application->all() as $possibleCommand) {
if ($possibleCommand::class == $commandClass) {
if (get_class($possibleCommand) == $commandClass) {
return $possibleCommand->getName();
}
}

@ -1,67 +0,0 @@
<?php
namespace jrosset\CliProgram\CommandCall;
use Traversable;
/**
* A list of arguments for a command call
*/
class CommandArgumentList {
/**
* @var array<string, mixed> The arguments
*/
protected array $arguments;
/**
* Initialization
*
* @param Traversable|null $arguments The arguments
*/
public function __construct (?Traversable $arguments = null) {
$this->arguments = [];
if ($arguments !== null) {
foreach ($arguments as $name => $value) {
$this->arguments[$name] = $value;
}
}
}
/**
* The arguments
*
* @return array<string, mixed> The arguments
*/
public function getArguments (): array {
return $this->arguments;
}
/**
* Add an argument to the list
*
* Replace it if existing.
*
* @param string $name The argument name
* @param mixed $value The argument value
*
* @return $this
*/
public function addArgument (string $name, mixed $value): static {
$this->arguments[$name] = $value;
return $this;
}
/**
* Add an option to the list
*
* Replace it if existing.
*
* @param string $name The option name (with or without the leading dashes)
* @param mixed $value The option value
*
* @return $this
*/
public function addOption (string $name, mixed $value = true): static {
$this->arguments[(str_starts_with($name, '-') ? '' : '--') . $name] = $value;
return $this;
}
}

@ -20,17 +20,17 @@ class CommandCall {
*/
private string $commandName;
/**
* @var CommandArgumentList The command arguments
* @var Arrayy<string, mixed> The command arguments
*/
private CommandArgumentList $commandArguments;
private Arrayy $commandArguments;
/**
* @param string|Command $commandName The new command name
* @param CommandArgumentList|Arrayy<string, mixed>|null $commandArguments The command new arguments
* @param null|Arrayy<string, mixed> $commandArguments The command new arguments
*/
public function __construct (string|Command $commandName, CommandArgumentList|Arrayy|null $commandArguments = null) {
public function __construct ($commandName, ?Arrayy $commandArguments = null) {
$this->setCommandName($commandName);
$this->setCommandArguments($commandArguments ?? new CommandArgumentList());
$this->setCommandArguments($commandArguments ?? new Arrayy());
}
/**
@ -40,12 +40,9 @@ class CommandCall {
*/
public function generateInput (): InputInterface {
return new ArrayInput(
array_merge(
[
'command' => $this->getCommandName(),
],
$this->getCommandArguments()->getArguments()
)
(clone $this->getCommandArguments())
->prepend($this->getCommandName(), 'command')
->toArray()
);
}
/**
@ -77,9 +74,9 @@ class CommandCall {
*
* @return $this
*/
public function setCommandName (string|Command $commandName): self {
public function setCommandName ($commandName): self {
$this->commandName = $commandName instanceof Command
? CliHelper::getCommandNameFromClass($commandName->getApplication(), $commandName::class)
? CliHelper::getCommandNameFromClass($commandName->getApplication(), get_class($commandName))
: $commandName;
return $this;
}
@ -89,18 +86,18 @@ class CommandCall {
*
* @return Arrayy<string, mixed> The command arguments
*/
public function getCommandArguments (): CommandArgumentList {
public function getCommandArguments (): Arrayy {
return $this->commandArguments;
}
/**
* Set the command arguments
*
* @param CommandArgumentList|Arrayy $commandArguments The command new arguments
* @param Arrayy<string, mixed> $commandArguments The command new arguments
*
* @return $this
*/
public function setCommandArguments (CommandArgumentList|Arrayy $commandArguments): self {
$this->commandArguments = $commandArguments instanceof CommandArgumentList ? $commandArguments : new CommandArgumentList($commandArguments);
public function setCommandArguments (Arrayy $commandArguments): self {
$this->commandArguments = $commandArguments;
return $this;
}
}

@ -3,7 +3,6 @@
namespace jrosset\CliProgram\Monolog;
use jrosset\CliProgram\Output\OutputWithLogger;
use Monolog\Level;
use Monolog\Logger;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\OutputInterface;
@ -17,24 +16,24 @@ class ConsoleOutputWithMonolog extends OutputWithLogger {
/**
* @inheritDoc
*/
protected function getLoggerLevelFromVerbosity (int $verbosity): Level {
protected function getLoggerLevelFromVerbosity (int $verbosity): int {
if ($verbosity <= OutputInterface::VERBOSITY_QUIET) {
return Level::Error;
return Logger::ERROR;
}
elseif ($verbosity <= OutputInterface::VERBOSITY_NORMAL) {
return Level::Notice;
return Logger::NOTICE;
}
elseif ($verbosity <= OutputInterface::VERBOSITY_VERBOSE) {
return Level::Info;
return Logger::INFO;
}
elseif ($verbosity <= OutputInterface::VERBOSITY_VERY_VERBOSE) {
return Level::Info;
return Logger::INFO;
}
elseif ($verbosity <= OutputInterface::VERBOSITY_DEBUG) {
return Level::Debug;
return Logger::DEBUG;
}
else {
return Level::Notice;
return Logger::NOTICE;
}
}
}

@ -50,7 +50,7 @@ trait TMonologApplication {
*
* @return $this
*/
public function setLogMainDirectory (string $logMainDirectory): static {
public function setLogMainDirectory (string $logMainDirectory): self {
$this->logMainDirectory = $logMainDirectory;
return $this;
}

@ -1,5 +1,4 @@
<?php
/** @noinspection PhpMissingFieldTypeInspection */
namespace jrosset\CliProgram\Output;
@ -33,9 +32,9 @@ class OutputWithLogger extends OutputWrapper {
* @param OutputInterface $output The real output interface
* @param TLogger|null $logger The logger
*
* @noinspection PhpDocSignatureInspection
* @noinspection PhpMissingParamTypeInspection
*/
public function __construct (OutputInterface $output, LoggerInterface $logger = null) {
public function __construct (OutputInterface $output, $logger = null) {
parent::__construct($output);
$this->setLogger($logger);
}
@ -43,8 +42,8 @@ class OutputWithLogger extends OutputWrapper {
/**
* @inheritDoc
*/
public function getErrorOutput (): static {
return new static(CliHelper::getErrorOutput($this->getOutput()), $this->logger);
public function getErrorOutput (): self {
return new static(CliHelper::getErrorOutput($this->getOutput()), $this->getLogger());
}
/**
@ -64,9 +63,13 @@ class OutputWithLogger extends OutputWrapper {
*
* @throws InvalidArgumentException If the logger is not null or an instance for {@see LoggerInterface}
*
* @noinspection PhpDocSignatureInspection
* @noinspection PhpMissingParamTypeInspection
*/
public function setLogger (LoggerInterface $logger): self {
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;
}
@ -80,7 +83,7 @@ class OutputWithLogger extends OutputWrapper {
*
* @return void
*/
protected function writeToLogger (iterable|string $messages, bool $newline = false, int $options = 0): 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;
}
@ -109,23 +112,24 @@ class OutputWithLogger extends OutputWrapper {
* @param int $verbosity The verbosity
*
* @return mixed The logger level
* @noinspection PhpReturnDocTypeMismatchInspection
*/
protected function getLoggerLevelFromVerbosity (int $verbosity): mixed {
protected function getLoggerLevelFromVerbosity (int $verbosity) {
return $verbosity;
}
/**
* @inheritDoc
*/
public function write (iterable|string $messages, bool $newline = false, int $options = 0) {
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 (iterable|string $messages, int $options = 0) {
public function writeLn ($messages, int $options = 0) {
$this->writeToLogger($messages, true, $options);
return parent::writeln($messages, $options);
return parent::writeLn($messages, $options);
}
}

@ -37,20 +37,20 @@ abstract class OutputWrapper implements OutputInterface {
*
* @return static The “error” output
*/
public function getErrorOutput (): static {
public function getErrorOutput (): self {
return new static(CliHelper::getErrorOutput($this->getOutput()));
}
/**
* @inheritDoc
*/
public function write (iterable|string $messages, bool $newline = false, int $options = 0) {
public function write ($messages, bool $newline = false, int $options = 0) {
return $this->getOutput()->write($messages, $newline, $options);
}
/**
* @inheritDoc
*/
public function writeln (iterable|string $messages, int $options = 0) {
public function writeln ($messages, int $options = 0) {
return $this->getOutput()->writeln($messages, $options);
}

@ -3,7 +3,6 @@
namespace jrosset\CliProgram\Requirements;
use jrosset\CliProgram\CliHelper;
use ReflectionAttribute;
use ReflectionClass;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
@ -26,7 +25,7 @@ trait TRequirementsApplication {
*/
protected final function registerCommandRequirementsListener (?EventDispatcherInterface $dispatcher = null): void {
$dispatcher ??= new EventDispatcher();
$dispatcher->addListener(ConsoleEvents::COMMAND, $this->checkCommandRequirements(...));
$dispatcher->addListener(ConsoleEvents::COMMAND, [$this, 'checkCommandRequirements']);
$this->setDispatcher($dispatcher);
}
@ -37,7 +36,7 @@ trait TRequirementsApplication {
*
* @return void
*/
private function checkCommandRequirements (ConsoleCommandEvent $event): void {
public function checkCommandRequirements (ConsoleCommandEvent $event): void {
$commandInput = $event->getInput();
$commandOutput = $event->getOutput();
try {
@ -48,13 +47,6 @@ trait TRequirementsApplication {
$commandReflection = new ReflectionClass($command);
//endregion
//region Check attribute requirements
foreach ($commandReflection->getAttributes(IRequirements::class, ReflectionAttribute::IS_INSTANCEOF) as $commandRequirementsAttribute) {
/** @var IRequirements $commandRequirementsAttributeInstance */
$commandRequirementsAttributeInstance = $commandRequirementsAttribute->newInstance();
$commandRequirementsAttributeInstance->checkRequirements($commandInput, $commandOutput);
}
//endregion
//region Contrôle pré-requis (implémentation directe)
if ($command instanceof IRequirements) {
$command->checkRequirements($commandInput, $commandOutput);

@ -7,12 +7,12 @@ use jrosset\CliProgram\Validation\Validators\InvalidValueException;
use jrosset\CliProgram\Validation\Validators\IValidator;
use ReflectionFunction;
use Stringable;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Exception\InvalidArgumentException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
/**
* Provide a value validation process ({@see IValidator}) to {@see self::addArgument()} and {@see self::addOption()}
@ -34,6 +34,8 @@ trait TCommandWithValidation {
/**
* @inheritDoc
*
* @throws Throwable If an argument or option is not valid
*/
protected function initialize (InputInterface $input, OutputInterface $output): void {
parent::setCode(
@ -49,8 +51,10 @@ trait TCommandWithValidation {
}
/**
* @inheritDoc
*
* @noinspection PhpMissingReturnTypeInspection
*/
public function setCode (callable $code): static {
public function setCode (callable $code) {
if ($code instanceof Closure) {
/** @noinspection PhpUnhandledExceptionInspection */
$codeReflection = new ReflectionFunction($code);
@ -69,9 +73,6 @@ trait TCommandWithValidation {
}
}
}
else {
$code = $code(...);
}
// {@see Command::$code} is used to perform input validation before execution, so set {@see self::$realCode} instead
$this->realCode = $code;
@ -193,7 +194,6 @@ trait TCommandWithValidation {
* @param mixed $default The default value (for InputArgument::OPTIONAL mode only)
* @param string $description The argument description
* @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
*
@ -203,21 +203,15 @@ trait TCommandWithValidation {
string $name,
int $mode = null,
string $description = '',
mixed $default = null,
?IValidator $validator = null,
Closure|array|null $suggestedValues = null
): static {
$default = null,
?IValidator $validator = null
): self {
$default = static::treatStringableDefaultValue($default);
if ($validator !== null) {
$default = $validator->getValidDefault($default);
}
if ($suggestedValues !== null) {
parent::addArgument($name, $mode, $description, $default, $suggestedValues);
}
else {
parent::addArgument($name, $mode, $description, $default);
}
return $this->setArgumentValidator($name, $validator);
}
/**
@ -230,7 +224,7 @@ trait TCommandWithValidation {
*
* @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)) {
throw new InvalidArgumentException(sprintf('The "%s" argument does not exist', $name));
}
@ -266,7 +260,6 @@ trait TCommandWithValidation {
* @param string $description The argument description
* @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 Closure|array|null $suggestedValues The function or list of suggested values when completing ; Null if none
*
* @return $this
*
@ -274,24 +267,18 @@ trait TCommandWithValidation {
*/
public function addOption (
string $name,
string|array|null $shortcut = null,
$shortcut = null,
int $mode = null,
string $description = '',
mixed $default = null,
?IValidator $validator = null,
Closure|array|null $suggestedValues = null
): static {
$default = null,
?IValidator $validator = null
): self {
$default = static::treatStringableDefaultValue($default);
if ($validator !== null) {
$default = $validator->getValidDefault($default);
}
if ($suggestedValues !== null) {
parent::addOption($name, $shortcut, $mode, $description, $default, $suggestedValues);
}
else {
parent::addOption($name, $shortcut, $mode, $description, $default);
}
return $this->setOptionValidator($name, $validator);
}
/**
@ -304,7 +291,7 @@ trait TCommandWithValidation {
*
* @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)) {
throw new InvalidArgumentException(sprintf('The "--%s" option does not exist', $name));
}
@ -338,7 +325,7 @@ trait TCommandWithValidation {
*
* @return mixed The default value
*/
protected static function treatStringableDefaultValue (mixed $default): mixed {
protected static function treatStringableDefaultValue ($default) {
if ($default instanceof Stringable) {
return $default->__toString();
}

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

@ -11,14 +11,16 @@ abstract class BasedValidator implements IValidator {
/**
* @var TValidator The internal validator
*/
private IValidator $internalValidator;
private $internalValidator;
/**
* Create a validator
*
* @param TValidator $internalValidator The internal validator
*
* @noinspection PhpMissingParamTypeInspection
*/
public function __construct (IValidator $internalValidator) {
public function __construct ($internalValidator) {
$this->internalValidator = $internalValidator;
}
/**
@ -26,14 +28,14 @@ abstract class BasedValidator implements IValidator {
*
* @return TValidator The internal validator
*/
protected function getInternalValidator (): IValidator {
protected function getInternalValidator () {
return $this->internalValidator;
}
/**
* @inheritDoc
*/
public function validate (mixed $value): bool {
public function validate ($value): bool {
return $this->internalValidator->validate($value);
}
}

@ -15,27 +15,21 @@ class DateTimeValidator extends AbstractDateTimeValidator {
* @var bool Is the time part mandatory ?
*/
private bool $timeMandatory;
/**
* @var DateTimeInterface The time part (if not mandatory)
*/
private DateTimeInterface $timePart;
/**
* Create a validator
*
* @param bool $timeMandatory Is the time part mandatory ?
* @param DateTimeInterface|null $timePart The time part (if not mandatory)
* @param bool $mandatoryDate Is the time part mandatory ?
*/
public function __construct (bool $timeMandatory = false, ?DateTimeInterface $timePart = null) {
$this->setTimeMandatory($timeMandatory);
$this->setTimePart($timePart ?? (new DateTimeImmutable())->setTimestamp(0));
public function __construct (bool $mandatoryDate = false) {
$this->setTimeMandatory($mandatoryDate);
parent::__construct();
}
/**
* @inheritDoc
*/
public function getValidDefault (mixed $default): string|bool|int|float|array|null {
public function getValidDefault ($default) {
if ($default instanceof DateTimeInterface) {
return $default->format('Y-m-d H:i:s');
}
@ -62,9 +56,9 @@ class DateTimeValidator extends AbstractDateTimeValidator {
(int)$this->getInternalValidator()->getMatch('day'),
);
return $value->setTime(
(int)($this->getInternalValidator()->getMatch('hour') ?? (int)$this->getTimePart()->format('%H')),
(int)($this->getInternalValidator()->getMatch('minute') ?? (int)$this->getTimePart()->format('%i')),
(int)($this->getInternalValidator()->getMatch('second') ?? (int)$this->getTimePart()->format('%s')),
(int)($this->getInternalValidator()->getMatch('hour') ?? 0),
(int)($this->getInternalValidator()->getMatch('minute') ?? 0),
(int)($this->getInternalValidator()->getMatch('second') ?? 0),
);
}
@ -87,24 +81,4 @@ class DateTimeValidator extends AbstractDateTimeValidator {
$this->timeMandatory = $timeMandatory;
return $this;
}
/**
* The time part (if not mandatory)
*
* @return DateTimeInterface The time part (if not mandatory)
*/
public function getTimePart (): DateTimeInterface {
return $this->timePart;
}
/**
* Set the time part (if not mandatory)
*
* @param DateTimeInterface $timePart The time part
*
* @return $this
*/
public function setTimePart (DateTimeInterface $timePart): self {
$this->timePart = $timePart;
return $this;
}
}

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

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

@ -21,7 +21,7 @@ class DirectoryValidator implements IValidator {
*
* @param FilesystemValidationOptionsList|FilesystemValidationOption[]|FilesystemValidationOption|null $options The options
*/
public function __construct (FilesystemValidationOptionsList|array|FilesystemValidationOption|null $options = null) {
public function __construct ($options = null) {
$this->setOptions($options);
}
@ -35,7 +35,7 @@ class DirectoryValidator implements IValidator {
* @throws InvalidArgumentException If the default value is not a string or null
* @throws InvalidArgumentException If the default directory doesn't match the options
*/
public function getValidDefault (mixed $default): string|bool|int|float|array|null {
public function getValidDefault ($default) {
if ($default === null) {
return null;
}
@ -43,13 +43,13 @@ class DirectoryValidator implements IValidator {
throw new InvalidArgumentException('The default value must be a string or null');
}
if ($this->getOptions()->contains(FilesystemValidationOption::IsReadable) && !is_readable($default)) {
if ($this->getOptions()->contains(FilesystemValidationOption::IS_READABLE) && !is_readable($default)) {
throw new InvalidArgumentException('The default value is not readable');
}
if ($this->getOptions()->contains(FilesystemValidationOption::IsWritable) && !is_writable($default)) {
if ($this->getOptions()->contains(FilesystemValidationOption::IS_WRITABLE) && !is_writable($default)) {
throw new InvalidArgumentException('The default value is not writable');
}
if ($this->getOptions()->contains(FilesystemValidationOption::MustExists) && !file_exists($default)) {
if ($this->getOptions()->contains(FilesystemValidationOption::MUST_EXISTS) && !file_exists($default)) {
throw new InvalidArgumentException('The default value doesn\'t exist');
}
return $default;
@ -58,18 +58,18 @@ class DirectoryValidator implements IValidator {
/**
* @inheritDoc
*/
public function validate (mixed $value): bool {
public function validate ($value): bool {
if (!is_string($value) && !$value instanceof Stringable) {
return false;
}
if ($this->getOptions()->contains(FilesystemValidationOption::IsReadable->name) && !is_readable($value)) {
if ($this->getOptions()->contains(FilesystemValidationOption::IS_READABLE) && !is_readable($value)) {
return false;
}
if ($this->getOptions()->contains(FilesystemValidationOption::IsWritable->name) && !is_writable($value)) {
if ($this->getOptions()->contains(FilesystemValidationOption::IS_WRITABLE) && !is_writable($value)) {
return false;
}
if ($this->getOptions()->contains(FilesystemValidationOption::MustExists->name) && !file_exists($value)) {
if ($this->getOptions()->contains(FilesystemValidationOption::MUST_EXISTS) && !file_exists($value)) {
return false;
}
@ -92,13 +92,22 @@ class DirectoryValidator implements IValidator {
*
* @return $this
*/
public function setOptions (FilesystemValidationOptionsList|array|FilesystemValidationOption|null $options = null): static {
$this->options = match (true) {
$options instanceof FilesystemValidationOptionsList => $options,
$options instanceof FilesystemValidationOption => new FilesystemValidationOptionsList([$options]),
is_array($options) => new FilesystemValidationOptionsList($options),
$options === null => new FilesystemValidationOptionsList(),
};
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;
}
}

@ -12,7 +12,7 @@ class EmailValidator implements IValidator {
/**
* @inheritDoc
*/
public function validate (mixed $value): bool {
public function validate ($value): bool {
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
return false;
}

@ -1,60 +0,0 @@
<?php
namespace jrosset\CliProgram\Validation\Validators;
use Arrayy\Type\StringCollection;
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 StringCollection();
foreach ($this->enum->getCases() as $enumCase) {
$enumCases->add($enumCase->getName());
}
parent::__construct(new ListValidator($enumCases));
}
/**
* @inheritDoc
*/
public function getValidDefault (mixed $default): string|bool|int|float|array|null {
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();
}
}

@ -19,7 +19,7 @@ class FileValidator extends DirectoryValidator {
* @param string|null $extensionsPattern The allowed extensions (regex pattern)
* @param FilesystemValidationOptionsList|FilesystemValidationOption[]|FilesystemValidationOption|null $options The options
*/
public function __construct (?string $extensionsPattern = null, FilesystemValidationOptionsList|FilesystemValidationOption|array|null $options = null) {
public function __construct (?string $extensionsPattern = null, $options = null) {
parent::__construct($options);
$this->setExtensionsPattern($extensionsPattern);
}
@ -27,7 +27,7 @@ class FileValidator extends DirectoryValidator {
/**
* @inheritDoc
*/
public function getValidDefault (mixed $default): string|bool|int|float|array|null {
public function getValidDefault ($default) {
if (($default = parent::getValidDefault($default)) === null) {
return null;
}
@ -39,7 +39,7 @@ class FileValidator extends DirectoryValidator {
/**
* @inheritDoc
*/
public function validate (mixed $value): bool {
public function validate ($value): bool {
if (!parent::validate($value)) {
return false;
}
@ -64,7 +64,7 @@ class FileValidator extends DirectoryValidator {
*
* @return $this
*/
public function setExtensionsPattern (?string $extensionsPattern = null): static {
public function setExtensionsPattern (?string $extensionsPattern = null): self {
$this->extensionsPattern = $extensionsPattern;
return $this;
}

@ -5,8 +5,8 @@ namespace jrosset\CliProgram\Validation\Validators;
/**
* Options of a filesystem based validator
*/
enum FilesystemValidationOption {
case MustExists;
case IsReadable;
case IsWritable;
abstract class FilesystemValidationOption {
public const MUST_EXISTS = 1;
public const IS_READABLE = 2;
public const IS_WRITABLE = 3;
}

@ -14,6 +14,6 @@ class FilesystemValidationOptionsList extends AbstractCollection {
* @inheritDoc
*/
public function getType (): string {
return FilesystemValidationOption::class;
return 'int';
}
}

@ -13,9 +13,9 @@ interface IValidator {
*
* @param mixed $default The initial/given default value
*
* @return string|bool|int|float|array|null The valid default value
* @return string|bool|int|float|array The valid default value
*/
public function getValidDefault (mixed $default): string|bool|int|float|array|null;
public function getValidDefault ($default);
/**
* Validate a value
@ -24,7 +24,7 @@ interface IValidator {
*
* @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
*

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

@ -32,7 +32,7 @@ class ListValidator implements IValidator {
/**
* @inheritDoc
*/
public function validate (mixed $value): bool {
public function validate ($value): bool {
if ($this->getAllowedValues()->contains($value)) {
$this->setValue($value);
return true;

@ -32,7 +32,7 @@ class RegexValidator implements IValidator {
/**
* @inheritDoc
*/
public function validate (mixed $value): bool {
public function validate ($value): bool {
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
*/
public function getMatch (int|string $group): ?string {
public function getMatch ($group): ?string {
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
*/
public function hasMatch (int|string $group): bool {
public function hasMatch ($group): bool {
return $this->getMatches()[$group] !== null;
}

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

@ -13,7 +13,7 @@ namespace jrosset\CliProgram\Validation\Validators;
*/
trait TInternalValueValidator {
/**
* @var TValue|null The current value, after it's validation
* @var TValue|null The current value, after
*/
private $value = null;
@ -32,7 +32,7 @@ trait TInternalValueValidator {
*
* @return $this
*/
protected function setValue ($value): static {
protected function setValue ($value): self {
$this->value = $value;
return $this;
}

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

@ -2,9 +2,10 @@
namespace jrosset\Tests\Commands;
use jrosset\Tests\FailedRequirement;
use LogicException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[FailedRequirement]
class FailedHello extends Hello {
/**
* @inheritDoc
@ -12,4 +13,11 @@ class FailedHello extends Hello {
public function __construct () {
parent::__construct('failedHello');
}
/**
* @inheritDoc
*/
public function checkRequirements (InputInterface $input, OutputInterface $output): void {
throw new LogicException('The "foo" requirement failed');
}
}

@ -4,28 +4,34 @@ namespace jrosset\Tests\Commands;
use DateTimeImmutable;
use DateTimeInterface;
use jrosset\CliProgram\CliCommand;
use jrosset\CliProgram\Output\OutputWithLogger;
use jrosset\CliProgram\Requirements\IRequirements;
use jrosset\CliProgram\Validation\CommandWithValidation;
use jrosset\CliProgram\Validation\Validators\DateValidator;
use jrosset\CliProgram\Validation\Validators\EnumValidator;
use jrosset\CliProgram\Validation\Validators\IntegerValidator;
use jrosset\Tests\Lang;
use jrosset\Tests\SuccessRequirement;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
#[CliCommand(null, 'Say hello')]
#[SuccessRequirement]
class Hello extends CommandWithValidation {
class Hello extends CommandWithValidation implements IRequirements {
/**
* @inheritdoc
*/
protected static $defaultDescription = 'Say hello';
/**
* @inheritDoc
*/
public function __construct (?string $name = null) {
parent::__construct($name ?? 'hello', ['bonjour']);
parent::__construct($name ?? 'hello', 'bonjour');
}
/**
* @inheritDoc
*/
public function checkRequirements (InputInterface $input, OutputInterface $output): void {
}
/**
@ -42,14 +48,6 @@ class Hello extends CommandWithValidation {
new DateValidator()
);
$this->addOption(
'lang',
'l',
InputOption::VALUE_REQUIRED,
'The lang',
null,
new EnumValidator(Lang::class)
);
$this->addOption(
'repeat',
'r',
@ -66,7 +64,7 @@ class Hello extends CommandWithValidation {
protected function execute (InputInterface $input, OutputInterface $output): int {
$output->writeln('<info>Command : ' . __CLASS__ . '</info>', OutputInterface::VERBOSITY_DEBUG);
$text = ($input->getOption('lang') ?? Lang::English)->value;
$text = 'Hello';
$repeat = $input->getOption('repeat');
/** @var DateTimeInterface $day */

@ -29,7 +29,7 @@ class ReadFile extends CommandWithValidation {
null,
new FileValidator(
/** @lang PhpRegExp */ '#\.txt$#i',
FilesystemValidationOption::IsReadable
FilesystemValidationOption::IS_READABLE
)
);
}

@ -1,19 +0,0 @@
<?php
namespace jrosset\Tests;
use Attribute;
use jrosset\CliProgram\Requirements\IRequirements;
use LogicException;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[Attribute(Attribute::TARGET_CLASS)]
class FailedRequirement implements IRequirements {
/**
* @inheritDoc
*/
public function checkRequirements (InputInterface $input, OutputInterface $output): void {
throw new LogicException('The "foo" requirement failed');
}
}

@ -1,9 +0,0 @@
<?php
namespace jrosset\Tests;
enum Lang: string {
case French = 'Bonjour';
case English = 'Good morning';
case American = 'Hello';
}

@ -1,17 +0,0 @@
<?php
namespace jrosset\Tests;
use Attribute;
use jrosset\CliProgram\Requirements\IRequirements;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[Attribute(Attribute::TARGET_CLASS)]
class SuccessRequirement implements IRequirements {
/**
* @inheritDoc
*/
public function checkRequirements (InputInterface $input, OutputInterface $output): void {
}
}
Loading…
Cancel
Save