diff --git a/composer.json b/composer.json index 9544158..413b9ce 100644 --- a/composer.json +++ b/composer.json @@ -14,12 +14,12 @@ }, "minimum-stability": "stable", - "require": { - "php": "^7.4 || ^8.0", - "symfony/console": "^5.4 || ^6.0", + "require": { + "php": "^8.1", + "symfony/console": "^6.1", "jrosset/betterphptoken": "^1.0", - "jrosset/collections": "^2.3 || ^3.0", - "jrosset/extendedmonolog": "^1.0 || ^2.0" + "jrosset/collections": "^3.0", + "jrosset/extendedmonolog": "^2.0" }, "autoload": { "psr-4": { diff --git a/src/CliProgram/Validation/CommandWithValidation.php b/src/CliProgram/Validation/CommandWithValidation.php new file mode 100644 index 0000000..c8cdc77 --- /dev/null +++ b/src/CliProgram/Validation/CommandWithValidation.php @@ -0,0 +1,13 @@ + The validators of each {@see InputArgument} + */ + private array $argumentsValidator = []; + /** + * @var array The validators of each {@see InputOption} + */ + private array $optionsValidator = []; + + /** + * @inheritDoc + * + * @throws Throwable If an argument or option is not valid + */ + protected function interact (InputInterface $input, OutputInterface $output): void { + parent::interact($input, $output); + $this->validate($input, $output); + } + /** + * Validate the command input + * + * @param InputInterface $input The input + * @param OutputInterface $output The output + * + * @return void + */ + protected function validate (InputInterface $input, OutputInterface $output): void { + $this->validateArguments($input, $output); + $this->validateOptions($input, $output); + } + /** + * Validate values of {@see InputArgument} + * + * @param InputInterface $input The input + * @param OutputInterface $output The output + * + * @return void + * + * @noinspection PhpUnusedParameterInspection + */ + protected function validateArguments (InputInterface $input, OutputInterface $output): void { + foreach ($this->argumentsValidator as $argumentName => $argumentValidator) { + if ($input->hasArgument($argumentName)) { + if (!$argumentValidator->validate($input->getArgument($argumentName))) { + throw new InvalidValueException(sprintf('The "%s" argument has not a valid value', $argumentName)); + } + $input->setArgument($argumentName, $argumentValidator->getValue()); + } + } + } + /** + * Validate values of {@see InputOption} + * + * @param InputInterface $input The input + * @param OutputInterface $output The output + * + * @return void + * + * @noinspection PhpUnusedParameterInspection + */ + protected function validateOptions (InputInterface $input, OutputInterface $output): void { + foreach ($this->optionsValidator as $optionName => $optionValidator) { + if ($input->hasOption($optionName)) { + if (!$optionValidator->validate($input->getOption($optionName))) { + throw new InvalidValueException(sprintf('The "--%s" option has not a valid value', $optionName)); + } + $input->setOption($optionName, $optionValidator->getValue()); + } + } + } + + /** + * Adds an argument + * + * @param string $name The argument name + * @param int|null $mode The argument mode: InputArgument::REQUIRED or InputArgument::OPTIONAL + * @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 + * + * @throws InvalidArgumentException When argument mode is not valid + */ + public function addArgument ( + string $name, + int $mode = null, + string $description = '', + mixed $default = null, + ?IValidator $validator = null, + Closure|array|null $suggestedValues = null + ): static { + $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); + } + /** + * Set the validator for an {@see InputArgument} + * + * @param string $name The {@see InputArgument} name (must exist) + * @param IValidator|null $validator The validator ; Null to remove it + * + * @return $this + * + * @throws InvalidArgumentException If the {@see InputArgument} doesn't exist + */ + public function setArgumentValidator (string $name, ?IValidator $validator): static { + if (!$this->getDefinition()->hasArgument($name)) { + throw new InvalidArgumentException(sprintf('The "%s" argument does not exist', $name)); + } + if ($validator === null) { + unset($this->argumentsValidator[$name]); + } + else { + $this->argumentsValidator[$name] = $validator; + } + + return $this; + } + /** + * Get the validator for an {@see InputArgument} + * + * @param string $name The {@see InputArgument} name (must exist) + * + * @return IValidator|null The validator or Null if none + */ + public function getArgumentValidator (string $name): ?IValidator { + if (!$this->getDefinition()->hasArgument($name)) { + throw new InvalidArgumentException(sprintf('The "%s" argument does not exist', $name)); + } + return $this->argumentsValidator[$name] ?? null; + } + + /** + * Adds an option + * + * @param string $name The argument name + * @param string|string[]|null $shortcut The shortcuts, can be null, a string of shortcuts delimited by | or an array of shortcuts + * @param int|null $mode The option mode: One of the InputOption::VALUE_* constants + * @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 + * + * @throws InvalidArgumentException When argument mode is not valid + */ + public function addOption ( + string $name, + string|array|null $shortcut = null, + int $mode = null, + string $description = '', + mixed $default = null, + ?IValidator $validator = null, + Closure|array|null $suggestedValues = null + ): static { + $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); + } + /** + * Set the validator for an {@see InputOption} + * + * @param string $name The {@see InputOption} name (must exist) + * @param IValidator|null $validator The validator ; Null to remove it + * + * @return $this + * + * @throws InvalidArgumentException If the {@see InputOption} doesn't exist + */ + public function setOptionValidator (string $name, ?IValidator $validator): static { + if (!$this->getDefinition()->hasOption($name)) { + throw new InvalidArgumentException(sprintf('The "--%s" option does not exist', $name)); + } + if ($validator === null) { + unset($this->argumentsValidator[$name]); + } + else { + $this->argumentsValidator[$name] = $validator; + } + + return $this; + } + /** + * Get the validator for an {@see InputOption} + * + * @param string $name The {@see InputOption} name (must exist) + * + * @return IValidator|null The validator or Null if none + */ + public function getOptionValidator (string $name): ?IValidator { + if (!$this->getDefinition()->hasOption($name)) { + throw new InvalidArgumentException(sprintf('The "--%s" option does not exist', $name)); + } + return $this->argumentsValidator[$name] ?? null; + } + + /** + * If it is the case, transform a {@see Stringable} default value into a string + * + * @param mixed $default The default value + * + * @return mixed The default value + */ + protected static function treatStringableDefaultValue (mixed $default): mixed { + if ($default instanceof Stringable) { + return $default->__toString(); + } + return $default; + } +} \ No newline at end of file diff --git a/src/CliProgram/Validation/Validators/AbstractDateTimeValidator.php b/src/CliProgram/Validation/Validators/AbstractDateTimeValidator.php new file mode 100644 index 0000000..7690ead --- /dev/null +++ b/src/CliProgram/Validation/Validators/AbstractDateTimeValidator.php @@ -0,0 +1,38 @@ + + */ +abstract class AbstractDateTimeValidator extends BasedValidator { + /** + * Create a validator + */ + public function __construct () { + parent::__construct(new RegexValidator($this->getPattern())); + } + + /** + * @inheritDoc + */ + public function validate (mixed $value): bool { + $this->getInternalValidator()->setPattern($this->getPattern()); + return parent::validate($value); + } + + /** + * The date and/or time regex pattern + * + * @return string The date and/or time regex pattern + */ + protected abstract function getPattern (): string; + /** + * @inheritDoc + */ + public abstract function getValue (): DateTimeInterface; +} \ No newline at end of file diff --git a/src/CliProgram/Validation/Validators/BasedValidator.php b/src/CliProgram/Validation/Validators/BasedValidator.php new file mode 100644 index 0000000..8dcbe9d --- /dev/null +++ b/src/CliProgram/Validation/Validators/BasedValidator.php @@ -0,0 +1,39 @@ +internalValidator = $internalValidator; + } + /** + * The internal validator + * + * @return TValidator The internal validator + */ + protected function getInternalValidator () { + return $this->internalValidator; + } + + /** + * @inheritDoc + */ + public function validate (mixed $value): bool { + return $this->internalValidator->validate($value); + } +} \ No newline at end of file diff --git a/src/CliProgram/Validation/Validators/DateTimeValidator.php b/src/CliProgram/Validation/Validators/DateTimeValidator.php new file mode 100644 index 0000000..de37e6a --- /dev/null +++ b/src/CliProgram/Validation/Validators/DateTimeValidator.php @@ -0,0 +1,85 @@ + + */ +class DateTimeValidator extends AbstractDateTimeValidator { + /** + * @var bool Is the time part mandatory ? + */ + private bool $timeMandatory; + + /** + * Create a validator + * + * @param bool $mandatoryDate Is the time part mandatory ? + */ + public function __construct (bool $mandatoryDate = false) { + $this->setTimeMandatory($mandatoryDate); + parent::__construct(); + } + + /** + * @inheritDoc + */ + public function getValidDefault (mixed $default): string|bool|int|float|array { + if ($default instanceof DateTimeInterface) { + return $default->format('Y-m-d H:i:s'); + } + return $default; + } + + /** + * @inheritDoc + */ + protected function getPattern (): string { + /** @noinspection RegExpUnnecessaryNonCapturingGroup */ + return /** @lang PhpRegExp */ '/^(?\d{4})-(?\d{2})-(?\d{2})(?: (?\d{2}):(?\d{2})(?::(?\d{2}))?)' . ($this->isTimeMandatory() ? '' : '?') + . '$/'; + } + + /** + * @inheritDoc + */ + public function getValue (): DateTimeInterface { + $value = new DateTimeImmutable(); + $value->setDate( + (int)$this->getInternalValidator()->getMatch('year'), + (int)$this->getInternalValidator()->getMatch('month'), + (int)$this->getInternalValidator()->getMatch('day'), + ); + $value->setTime( + (int)($this->getInternalValidator()->getMatch('hour') ?? 0), + (int)($this->getInternalValidator()->getMatch('minute') ?? 0), + (int)($this->getInternalValidator()->getMatch('second') ?? 0), + ); + return $value; + } + + /** + * Is the time part mandatory ? + * + * @return bool Is the time part mandatory ? + */ + public function isTimeMandatory (): bool { + return $this->timeMandatory; + } + /** + * Is the time part mandatory ? + * + * @param bool $timeMandatory Is the time part mandatory ? + * + * @return $this + */ + public function setTimeMandatory (bool $timeMandatory): self { + $this->timeMandatory = $timeMandatory; + return $this; + } +} \ No newline at end of file diff --git a/src/CliProgram/Validation/Validators/DateValidator.php b/src/CliProgram/Validation/Validators/DateValidator.php new file mode 100644 index 0000000..c7911dd --- /dev/null +++ b/src/CliProgram/Validation/Validators/DateValidator.php @@ -0,0 +1,82 @@ + + */ +class DateValidator extends AbstractDateTimeValidator { + /** + * @var DateTimeInterface The time part + */ + private DateTimeInterface $timePart; + + /** + * Create a validator + * + * @param DateTimeInterface|null $datePart The time part + */ + public function __construct (?DateTimeInterface $datePart = null) { + $this->setTimePart($datePart ?? (new DateTimeImmutable())->setTimestamp(0)); + parent::__construct(); + } + + /** + * @inheritDoc + */ + public function getValidDefault (mixed $default): string|bool|int|float|array { + if ($default instanceof DateTimeInterface) { + return $default->format('Y-m-d'); + } + return $default; + } + + /** + * @inheritDoc + */ + protected function getPattern (): string { + return /** @lang PhpRegExp */ '/^(?\d{4})-(?\d{2})-(?\d{2})$/'; + } + /** + * @inheritDoc + */ + public function getValue (): DateTimeInterface { + $value = new DateTimeImmutable(); + $value->setDate( + (int)$this->getInternalValidator()->getMatch('year'), + (int)$this->getInternalValidator()->getMatch('month'), + (int)$this->getInternalValidator()->getMatch('day'), + ); + $value->setTime( + (int)$this->getTimePart()->format('%H'), + (int)$this->getTimePart()->format('%i'), + (int)$this->getTimePart()->format('%s'), + ); + return $value; + } + + /** + * The time part + * + * @return DateTimeInterface The time part + */ + public function getTimePart (): DateTimeInterface { + return $this->timePart; + } + /** + * Set the time part + * + * @param DateTimeInterface $timePart The time part + * + * @return $this + */ + public function setTimePart (DateTimeInterface $timePart): self { + $this->timePart = $timePart; + return $this; + } +} \ No newline at end of file diff --git a/src/CliProgram/Validation/Validators/DecimalValidator.php b/src/CliProgram/Validation/Validators/DecimalValidator.php new file mode 100644 index 0000000..13f8eef --- /dev/null +++ b/src/CliProgram/Validation/Validators/DecimalValidator.php @@ -0,0 +1,135 @@ + + * @template-implements TInternalValueValidator + */ +class DecimalValidator extends BasedValidator { + use TIdenticalValidDefaultValidator; + use TInternalValueValidator; + + /** + * @var string The locale thousands separator + */ + private string $decimalSeparator; + /** + * @var string The locale thousands separator + */ + private string $thousandsSeparator; + /** + * @var float|null The minimal value allowed, Null if none + */ + private ?float $minAllowedValue = null; + /** + * @var float|null The maximal value allowed, Null if none + */ + private ?float $maxAllowedValue = null; + + /** + * Create a validator + * + * @param float|null $minAllowedValue The minimal value allowed, Null if none + * @param float|null $maxAllowedValue The maximal value allowed, Null if none + */ + public function __construct (?float $minAllowedValue, ?float $maxAllowedValue) { + $locale = localeconv(); + $this->decimalSeparator = preg_quote($locale['decimal_point'], '/'); + $this->thousandsSeparator = preg_quote($locale['thousands_sep'], '/'); + $this->setMinAllowedValue($minAllowedValue); + $this->setMaxAllowedValue($maxAllowedValue); + $this->value = 0; + + parent::__construct( + new RegexValidator( + /** @lang PhpRegExp */ '/^(?[+-]?(?:\d{1,3}' . $this->thousandsSeparator . '?(?:\d{3}' . $this->thousandsSeparator . '?)*\d{3}|\d{1,3}))(?:' . $this->decimalSeparator + . '(?\d+))?$/' + ) + ); + } + + /** + * @inheritDoc + */ + public function validate (mixed $value): bool { + if (!parent::validate($value)) { + return false; + } + + $this->setValue( + (float)( + preg_replace( + [ + '/^+/', + '/' . $this->thousandsSeparator . '/', + ], + [ + '', + '', + ], + $this->getInternalValidator()->getMatch('int') + ) + . '.' . $this->getInternalValidator()->getMatch('dex') + ) + ); + if ($this->getMinAllowedValue() !== null && $value < $this->getMinAllowedValue()) { + return false; + } + if ($this->getMaxAllowedValue() !== null && $value > $this->getMaxAllowedValue()) { + return false; + } + + return true; + } + + /** + * The minimal value allowed, Null if none + * + * @return float|null The minimal value allowed, Null if none + */ + public function getMinAllowedValue (): ?float { + return $this->minAllowedValue; + } + /** + * Set the minimal value allowed + * + * @param float|null $minAllowedValue The minimal value allowed, Null if none + * + * @return $this + */ + public function setMinAllowedValue (?float $minAllowedValue): self { + if ($minAllowedValue !== null && $this->getMaxAllowedValue() !== null && $minAllowedValue > $this->getMaxAllowedValue()) { + throw new InvalidArgumentException('The minimal allowed value must be null or lesser than the maximal allowed value'); + } + $this->minAllowedValue = $minAllowedValue; + return $this; + } + + /** + * The maximal value allowed, Null if none + * + * @return float|null The maximal value allowed, Null if none + */ + public function getMaxAllowedValue (): ?float { + return $this->maxAllowedValue; + } + /** + * Set the maximal value allowed + * + * @param float|null $maxAllowedValue The maximal value allowed, Null if none + * + * @return $this + */ + public function setMaxAllowedValue (?float $maxAllowedValue): self { + if ($maxAllowedValue !== null && $this->getMinAllowedValue() !== null && $maxAllowedValue < $this->getMinAllowedValue()) { + throw new InvalidArgumentException('The maximal allowed value must be null or greater than the minimal allowed value'); + } + $this->maxAllowedValue = $maxAllowedValue; + return $this; + } +} \ No newline at end of file diff --git a/src/CliProgram/Validation/Validators/IValidator.php b/src/CliProgram/Validation/Validators/IValidator.php new file mode 100644 index 0000000..2190202 --- /dev/null +++ b/src/CliProgram/Validation/Validators/IValidator.php @@ -0,0 +1,34 @@ + + * @template-implements TInternalValueValidator + */ +class IntegerValidator extends BasedValidator { + use TIdenticalValidDefaultValidator; + use TInternalValueValidator; + + /** + * @var string The locale thousands separator + */ + private string $thousandsSeparator; + /** + * @var int|null The minimal value allowed, Null if none + */ + private ?int $minAllowedValue = null; + /** + * @var int|null The maximal value allowed, Null if none + */ + private ?int $maxAllowedValue = null; + + /** + * Create a validator + * + * @param int|null $minAllowedValue The minimal value allowed, Null if none + * @param int|null $maxAllowedValue The maximal value allowed, Null if none + */ + public function __construct (?int $minAllowedValue, ?int $maxAllowedValue) { + $locale = localeconv(); + $this->thousandsSeparator = preg_quote($locale['thousands_sep'], '/'); + $this->setMinAllowedValue($minAllowedValue); + $this->setMaxAllowedValue($maxAllowedValue); + $this->value = 0; + + parent::__construct( + new RegexValidator( + /** @lang PhpRegExp */ '/^[+-]?(?:\d{1,3}' . $this->thousandsSeparator . '?(?:\d{3}' . $this->thousandsSeparator . '?)*\d{3}|\d{1,3})$/' + ) + ); + } + + /** + * @inheritDoc + */ + public function validate (mixed $value): bool { + if (!parent::validate($value)) { + return false; + } + + $this->setValue( + (int)preg_replace( + [ + '/^\+/', + '/' . $this->thousandsSeparator . '/', + ], + [ + '', + '', + ], + $value + ) + ); + if ($this->getMinAllowedValue() !== null && $value < $this->getMinAllowedValue()) { + return false; + } + if ($this->getMaxAllowedValue() !== null && $value > $this->getMaxAllowedValue()) { + return false; + } + + return true; + } + + /** + * The minimal value allowed, Null if none + * + * @return int|null The minimal value allowed, Null if none + */ + public function getMinAllowedValue (): ?int { + return $this->minAllowedValue; + } + /** + * Set the minimal value allowed + * + * @param int|null $minAllowedValue The minimal value allowed, Null if none + * + * @return $this + */ + public function setMinAllowedValue (?int $minAllowedValue): self { + if ($minAllowedValue !== null && $this->getMaxAllowedValue() !== null && $minAllowedValue > $this->getMaxAllowedValue()) { + throw new InvalidArgumentException('The minimal allowed value must be null or lesser than the maximal allowed value'); + } + $this->minAllowedValue = $minAllowedValue; + return $this; + } + + /** + * The maximal value allowed, Null if none + * + * @return int|null The maximal value allowed, Null if none + */ + public function getMaxAllowedValue (): ?int { + return $this->maxAllowedValue; + } + /** + * Set the maximal value allowed + * + * @param int|null $maxAllowedValue The maximal value allowed, Null if none + * + * @return $this + */ + public function setMaxAllowedValue (?int $maxAllowedValue): self { + if ($maxAllowedValue !== null && $this->getMinAllowedValue() !== null && $maxAllowedValue < $this->getMinAllowedValue()) { + throw new InvalidArgumentException('The maximal allowed value must be null or greater than the minimal allowed value'); + } + $this->maxAllowedValue = $maxAllowedValue; + return $this; + } +} \ No newline at end of file diff --git a/src/CliProgram/Validation/Validators/InvalidValueException.php b/src/CliProgram/Validation/Validators/InvalidValueException.php new file mode 100644 index 0000000..530ec85 --- /dev/null +++ b/src/CliProgram/Validation/Validators/InvalidValueException.php @@ -0,0 +1,11 @@ + + * @template-implements TInternalValueValidator + */ +class ListValidator implements IValidator { + use TIdenticalValidDefaultValidator; + use TInternalValueValidator; + + /** + * @var ICollection The list of allowed values + */ + private ICollection $allowedValues; + + /** + * Create a validator + * + * @param ICollection|null $allowedValues The list of allowed values + */ + public function __construct (?ICollection $allowedValues = null) { + $this->setAllowedValues($allowedValues ?? new Collection()); + } + + /** + * @inheritDoc + */ + public function validate (mixed $value): bool { + if ($this->getAllowedValues()->contains($value)) { + $this->setValue($value); + return true; + } + return false; + } + + /** + * The list of allowed values + * + * @return ICollection The list of allowed values + */ + public function getAllowedValues (): ICollection { + return $this->allowedValues; + } + /** + * Set the list of allowed values + * + * @param ICollection $allowedValues The list of allowed values + * + * @return $this + */ + public function setAllowedValues (ICollection $allowedValues): self { + $this->allowedValues = $allowedValues; + return $this; + } +} diff --git a/src/CliProgram/Validation/Validators/RegexValidator.php b/src/CliProgram/Validation/Validators/RegexValidator.php new file mode 100644 index 0000000..ec99856 --- /dev/null +++ b/src/CliProgram/Validation/Validators/RegexValidator.php @@ -0,0 +1,94 @@ +pattern = $pattern; + $this->matches = []; + } + + /** + * @inheritDoc + */ + public function validate (mixed $value): bool { + return preg_match($this->pattern, $value, $this->matches) === 1; + } + + /** + * The regex pattern + * + * @return string The regex pattern + */ + public function getPattern (): string { + return $this->pattern; + } + /** + * Set the regex pattern + * + * @param string $pattern The regex pattern + * + * @return $this + */ + public function setPattern (string $pattern): self { + $this->pattern = $pattern; + return $this; + } + + /** + * The capturing groups + * + * @return string[] The capturing groups + */ + public function getMatches (): array { + return $this->matches; + } + /** + * A capturing group + * + * @param int|string $group The group name/index + * + * @return string|null The capturing group or Null if not set + */ + public function getMatch (int|string $group): ?string { + return $this->getMatches()[$group] ?? null; + } + /** + * Has a capturing group ? + * + * @param int|string $group The group name/index + * + * @return bool True if the capturing group exists and is not null + */ + public function hasMatch (int|string $group): bool { + return $this->getMatches()[$group] !== null; + } + + /** + * @inheritDoc + */ + public function getValue (): ?string { + return $this->getMatch(static::GROUP_VALUE) ?? $this->getMatch(0); + } +} \ No newline at end of file diff --git a/src/CliProgram/Validation/Validators/TIdenticalValidDefaultValidator.php b/src/CliProgram/Validation/Validators/TIdenticalValidDefaultValidator.php new file mode 100644 index 0000000..8a159e8 --- /dev/null +++ b/src/CliProgram/Validation/Validators/TIdenticalValidDefaultValidator.php @@ -0,0 +1,17 @@ + + */ +trait TInternalValueValidator { + /** + * @var TValue|null The current value, after + */ + private $value = null; + + /** + * Get the value, after it's validation + * + * @return TValue|null The value + */ + public function getValue () { + return $this->value; + } + /** + * Set the value + * + * @param TValue $value The value (after it's validation) + * + * @return $this + */ + protected function setValue ($value): static { + $this->value = $value; + return $this; + } +} \ No newline at end of file diff --git a/src/CliProgram/Validation/Validators/TimeValidator.php b/src/CliProgram/Validation/Validators/TimeValidator.php new file mode 100644 index 0000000..beef772 --- /dev/null +++ b/src/CliProgram/Validation/Validators/TimeValidator.php @@ -0,0 +1,82 @@ + + */ +class TimeValidator extends AbstractDateTimeValidator { + /** + * @var DateTimeInterface The date part + */ + private DateTimeInterface $datePart; + + /** + * Create a validator + * + * @param DateTimeInterface|null $datePart The date part + */ + public function __construct (?DateTimeInterface $datePart = null) { + $this->setDatePart($datePart ?? (new DateTimeImmutable())->setTimestamp(0)); + parent::__construct(); + } + + /** + * @inheritDoc + */ + public function getValidDefault (mixed $default): string|bool|int|float|array { + if ($default instanceof DateTimeInterface) { + return $default->format('H:i:s'); + } + return $default; + } + + /** + * @inheritDoc + */ + protected function getPattern (): string { + return /** @lang PhpRegExp */ '/^(?\d{2}):(?\d{2})(?::(?\d{2}))?$/'; + } + /** + * @inheritDoc + */ + public function getValue (): DateTimeInterface { + $value = new DateTimeImmutable(); + $value->setDate( + (int)$this->getDatePart()->format('%Y'), + (int)$this->getDatePart()->format('%m'), + (int)$this->getDatePart()->format('%d'), + ); + $value->setTime( + (int)($this->getInternalValidator()->getMatch('hour') ?? 0), + (int)($this->getInternalValidator()->getMatch('minute') ?? 0), + (int)($this->getInternalValidator()->getMatch('second') ?? 0), + ); + return $value; + } + + /** + * The date part + * + * @return DateTimeInterface The date part + */ + public function getDatePart (): DateTimeInterface { + return $this->datePart; + } + /** + * Set the date part + * + * @param DateTimeInterface $datePart The date part + * + * @return $this + */ + public function setDatePart (DateTimeInterface $datePart): self { + $this->datePart = $datePart; + return $this; + } +} \ No newline at end of file diff --git a/tests/Commands/Hello.php b/tests/Commands/Hello.php index 1b81bc9..17c0844 100644 --- a/tests/Commands/Hello.php +++ b/tests/Commands/Hello.php @@ -2,13 +2,15 @@ namespace jrosset\Tests\Commands; -use jrosset\CliProgram\BaseCommand; use jrosset\CliProgram\Monolog\ConsoleOutputWithMonolog; +use jrosset\CliProgram\Validation\CommandWithValidation; +use jrosset\CliProgram\Validation\Validators\IntegerValidator; 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 Hello extends BaseCommand { +class Hello extends CommandWithValidation { /** * @inheritdoc */ @@ -21,12 +23,31 @@ class Hello extends BaseCommand { parent::__construct('hello', 'bonjour'); } + /** + * @inheritDoc + */ + protected function configure () { + parent::configure(); + + $this->addArgument( + 'repeat', + InputArgument::OPTIONAL, + 'The number of repeat', + 1, + new IntegerValidator(1, null) + ); + } + /** * @inheritDoc */ protected function execute (InputInterface $input, OutputInterface $output): int { $output->writeln('Command : ' . __CLASS__, OutputInterface::VERBOSITY_DEBUG); - $output->writeln('Hello !'); + $repeat = $input->getArgument('repeat'); + var_dump($repeat); + for ($curr = 0; $curr < $repeat; $curr++) { + $output->writeln('Hello !'); + } $output->writeln('FIN', ConsoleOutputWithMonolog::OPTION_SKIP_MONOLOG); return Command::SUCCESS; }