Add "validation" module
parent
a8d04db3f1
commit
6b77ed1c42
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation;
|
||||||
|
|
||||||
|
use jrosset\CliProgram\BaseCommand;
|
||||||
|
use jrosset\CliProgram\Validation\Validators\IValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A command with a value validation process ({@see IValidator}) to {@see self::addArgument()} and {@see self::addOption()}
|
||||||
|
*/
|
||||||
|
class CommandWithValidation extends BaseCommand {
|
||||||
|
use TCommandWithValidation;
|
||||||
|
}
|
@ -0,0 +1,250 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use jrosset\CliProgram\Validation\Validators\InvalidValueException;
|
||||||
|
use jrosset\CliProgram\Validation\Validators\IValidator;
|
||||||
|
use Stringable;
|
||||||
|
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()}
|
||||||
|
*/
|
||||||
|
trait TCommandWithValidation {
|
||||||
|
/**
|
||||||
|
* @var array<string, IValidator> The validators of each {@see InputArgument}
|
||||||
|
*/
|
||||||
|
private array $argumentsValidator = [];
|
||||||
|
/**
|
||||||
|
* @var array<string, IValidator> 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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
use DateTimeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An argument/option value validator expecting a date and/or time
|
||||||
|
*
|
||||||
|
* @template-implements BasedValidator<RegexValidator>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An argument/option value validator based on another, internal, validator
|
||||||
|
*
|
||||||
|
* @template TValidator of IValidator
|
||||||
|
*/
|
||||||
|
abstract class BasedValidator implements IValidator {
|
||||||
|
/**
|
||||||
|
* @var TValidator The internal validator
|
||||||
|
*/
|
||||||
|
private $internalValidator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a validator
|
||||||
|
*
|
||||||
|
* @param TValidator $internalValidator The internal validator
|
||||||
|
*/
|
||||||
|
public function __construct ($internalValidator) {
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An argument/option value validator expecting a date and time
|
||||||
|
*
|
||||||
|
* @template-implements BasedValidator<RegexValidator>
|
||||||
|
*/
|
||||||
|
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 */ '/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})(?: (?<hour>\d{2}):(?<minute>\d{2})(?::(?<second>\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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An argument/option value validator expecting a date (without time)
|
||||||
|
*
|
||||||
|
* @template-implements BasedValidator<RegexValidator>
|
||||||
|
*/
|
||||||
|
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 */ '/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,135 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An argument/option value validator expecting a decimal
|
||||||
|
*
|
||||||
|
* @template-implements BasedValidator<RegexValidator>
|
||||||
|
* @template-implements TInternalValueValidator<float>
|
||||||
|
*/
|
||||||
|
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 */ '/^(?<int>[+-]?(?:\d{1,3}' . $this->thousandsSeparator . '?(?:\d{3}' . $this->thousandsSeparator . '?)*\d{3}|\d{1,3}))(?:' . $this->decimalSeparator
|
||||||
|
. '(?<dec>\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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An argument/option value validator
|
||||||
|
*
|
||||||
|
* @template TValue
|
||||||
|
*/
|
||||||
|
interface IValidator {
|
||||||
|
/**
|
||||||
|
* Get a valid default value from the initial/given default value
|
||||||
|
*
|
||||||
|
* @param mixed $default The initial/given default value
|
||||||
|
*
|
||||||
|
* @return string|bool|int|float|array The valid default value
|
||||||
|
*/
|
||||||
|
public function getValidDefault (mixed $default): string|bool|int|float|array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate a value
|
||||||
|
*
|
||||||
|
* @param mixed $value The value to validate
|
||||||
|
*
|
||||||
|
* @return bool True if the value is valid, else False
|
||||||
|
*/
|
||||||
|
public function validate (mixed $value): bool;
|
||||||
|
/**
|
||||||
|
* Get the value, after it's validation
|
||||||
|
*
|
||||||
|
* @return TValue|null The value
|
||||||
|
*/
|
||||||
|
public function getValue ();
|
||||||
|
}
|
@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An argument/option value validator expecting an integer
|
||||||
|
*
|
||||||
|
* @template-implements BasedValidator<RegexValidator>
|
||||||
|
* @template-implements TInternalValueValidator<int>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An invalid value for an argument/option
|
||||||
|
*/
|
||||||
|
class InvalidValueException extends RuntimeException {
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
use jrosset\Collections\Collection;
|
||||||
|
use jrosset\Collections\ICollection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An argument/option value validator based on a list of value
|
||||||
|
*
|
||||||
|
* @template TValue
|
||||||
|
* @template-implements IValidator<TValue>
|
||||||
|
* @template-implements TInternalValueValidator<TValue>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An argument/option value validator based on a regex
|
||||||
|
*/
|
||||||
|
class RegexValidator implements IValidator {
|
||||||
|
use TIdenticalValidDefaultValidator;
|
||||||
|
|
||||||
|
public const GROUP_VALUE = 'value';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string The regex pattern
|
||||||
|
*/
|
||||||
|
private string $pattern;
|
||||||
|
/**
|
||||||
|
* @var string[] The capturing groups
|
||||||
|
*/
|
||||||
|
private array $matches;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a validator
|
||||||
|
*
|
||||||
|
* @param string $pattern The regex pattern
|
||||||
|
*/
|
||||||
|
public function __construct (string $pattern) {
|
||||||
|
$this->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);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation to get a valid default value identical to the initial/given default value
|
||||||
|
*
|
||||||
|
* @see IValidator
|
||||||
|
*/
|
||||||
|
trait TIdenticalValidDefaultValidator {
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
public function getValidDefault (mixed $default): string|bool|int|float|array {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
/** @noinspection PhpMissingFieldTypeInspection */
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation for an internal "value"
|
||||||
|
*
|
||||||
|
* Call {@see self::setValue()} to set the value
|
||||||
|
*
|
||||||
|
* @template TValue
|
||||||
|
* @template-implements IValidator<TValue>
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace jrosset\CliProgram\Validation\Validators;
|
||||||
|
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use DateTimeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An argument/option value validator expecting a date (without time)
|
||||||
|
*
|
||||||
|
* @template-implements BasedValidator<RegexValidator>
|
||||||
|
*/
|
||||||
|
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 */ '/^(?<hour>\d{2}):(?<minute>\d{2})(?::(?<second>\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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue