Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions src/BaseInputFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use function is_int;
use function is_iterable;
use function is_string;
use function iterator_to_array;
use function sprintf;

/**
Expand Down Expand Up @@ -218,6 +219,37 @@ public function isValid(array|null $context = null): bool
return $this->validateInputs($inputs, $this->data, $context);
}

public function validate(iterable|null $data, array|null $context = null): InputFilterValidationResult
{
$data = iterator_to_array($data ?? []);
$context ??= $data;
$inputs = $this->validationGroup ?? array_keys($this->inputs);
$results = [];
foreach ($inputs as $name) {
$input = $this->inputs[$name];
/** @psalm-var mixed $value */
$value = $data[$name] ?? null;

if ($input instanceof InputFilterInterface) {
$value = is_iterable($value) ? iterator_to_array($value) : [];

$result = $input->validate($value, $context);
assert($result instanceof InputFilterValidationResult);
$results[$name] = $result;

continue;
}

$result = $input->validate($value, $context);
$results[$name] = $result;
if (! $result->valid() && $input->breakOnFailure()) {
break;
}
}

return new InputFilterValidationResult($results);
}

/**
* Validate a set of inputs against the current data
*
Expand Down
60 changes: 58 additions & 2 deletions src/Input.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Laminas\Validator\ValidatorChain;
use Laminas\Validator\ValidatorChainInterface;

use function array_merge;
use function assert;
use function class_exists;
use function get_debug_type;
Expand Down Expand Up @@ -277,6 +278,62 @@ public function merge(InputInterface $input): static
return $this;
}

public function validate(mixed $value, array $context): InputValidationResult
{
$isEmpty = $value === '' || $value === null || $value === [];
/** @psalm-var mixed $resolvedValue */
$resolvedValue = $isEmpty && $this->hasFallback ? $this->fallbackValue : $value;
/**
* Behaviour Change: The fallback value is filtered where previously it was returned verbatim
*
* @psalm-var mixed $filteredValue
*/
$filteredValue = $this->filterChain->filter($resolvedValue);

if (
// We have a valid result when a value is empty, but a fallback is present
($isEmpty && $this->hasFallback)
||
// Empty values are valid when they are not required and validation should not continue for empty values
($isEmpty && ! $this->required && ! $this->continueIfEmpty)
||
// Empty is valid when allowEmpty is true and continue if empty is false
($isEmpty && $this->allowEmpty && ! $this->continueIfEmpty)
) {
return InputValidationResult::pass($this->name, $value, $filteredValue);
}

$isValid = $this->validatorChain->isValid($filteredValue, $context);
$messages = $this->validatorChain->getMessages();

/**
* An empty value should not be considered valid in this situation, regardless
* of what the validator chain says.
* Instead of mutating the chain, fail validation with a validation failure message that advises the user to
* customise the validation chain with a NotEmpty validator.
*/
if ($isValid && $isEmpty) {
$isValid = false;
$messages = array_merge([
InputInterface::EMPTY_FAILURE_VALIDATION_KEY => sprintf(
'The value for "%s" was empty, but its configuration prohibits an empty value. '
. 'Prepend a "NotEmpty" validator to this input’s chain in order to customise '
. 'this validation failure message',
$this->name,
),
], $messages);
}

return $isValid
? InputValidationResult::pass($this->name, $value, $filteredValue)
: InputValidationResult::fail(
$this->name,
$value,
$filteredValue,
new ErrorMessages($messages),
);
}

/** @inheritDoc */
public function isValid(array|null $context = null): bool
{
Expand Down Expand Up @@ -340,8 +397,7 @@ public function getMessages(): ErrorMessages
return new ErrorMessages([]);
}

$validator = $this->getValidatorChain();
return new ErrorMessages($validator->getMessages());
return new ErrorMessages($this->validatorChain->getMessages());
}

protected function injectNotEmptyValidator(): void
Expand Down
8 changes: 8 additions & 0 deletions src/InputFilterInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ public function setData(iterable|null $data): static;
*/
public function isValid(array|null $context = null): bool;

/**
* Is the data set valid?
*
* @param iterable<array-key, mixed>|null $data
* @param array<array-key, mixed>|null $context
*/
public function validate(iterable|null $data, array|null $context = null): InputFilterValidationResult;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public function validate(iterable|null $data, array|null $context = null): InputFilterValidationResult;
public function validate(iterable $data, array|null $context = null): InputFilterValidationResult;

Can you delegate to the user the typecast to iterable?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes - probably. This was mainly about sticking to the signature of setData() IIRC, but there's no reason to do that. I'd also say that $context could be array $context = [] and we drop the null union.


/**
* Provide a list of one or more elements indicating the complete set to validate
*
Expand Down
82 changes: 82 additions & 0 deletions src/InputFilterValidationResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Laminas\InputFilter;

use Laminas\InputFilter\Exception\InputNotFoundException;

final readonly class InputFilterValidationResult implements ValidationResultInterface
{
/** @param array<array-key, self|InputValidationResult> $results */
public function __construct(
public array $results,
) {
}

public function valid(): bool
{
foreach ($this->results as $result) {
if ($result->valid()) {
continue;
}

return false;
}

return true;
}

public function getMessages(): ErrorMessages
{
$messages = [];
foreach ($this->results as $key => $result) {
$name = $this->keyName($key, $result);
$messages[$name] = $result->getMessages();
}

return new ErrorMessages($messages);
}

/** @psalm-suppress MixedAssignment */
public function rawValue(): array
{
$value = [];
foreach ($this->results as $key => $result) {
$name = $this->keyName($key, $result);
$value[$name] = $result->rawValue();
}

return $value;
}

/** @psalm-suppress MixedAssignment */
public function value(): array
{
$value = [];
foreach ($this->results as $key => $result) {
$name = $this->keyName($key, $result);
$value[$name] = $result->value();
}

return $value;
}

/** @throws InputNotFoundException */
public function resultFor(string|int $key): ValidationResultInterface
{
$result = $this->results[$key] ?? null;
if (! $result instanceof ValidationResultInterface) {
throw InputNotFoundException::forKey($key);
}

return $result;
}

private function keyName(string|int $key, ValidationResultInterface $result): string|int
{
return $result instanceof InputValidationResult
? $result->name()
: $key;
}
}
6 changes: 6 additions & 0 deletions src/InputInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

interface InputInterface
{
/** @internal */
public const EMPTY_FAILURE_VALIDATION_KEY = '__inputEmptyValueFailure';

public function setValue(mixed $value): static;

public function allowEmpty(): bool;
Expand All @@ -33,6 +36,9 @@ public function getValue(): mixed;
/** @param array<array-key, mixed>|null $context */
public function isValid(array|null $context = null): bool;

/** @param array<array-key, mixed> $context */
public function validate(mixed $value, array $context): InputValidationResult;

public function getMessages(): ErrorMessages;

public function continueIfEmpty(): bool;
Expand Down
59 changes: 59 additions & 0 deletions src/InputValidationResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Laminas\InputFilter;

final readonly class InputValidationResult implements ValidationResultInterface
{
private function __construct(
private int|string $name,
private mixed $rawValue,
private mixed $value,
private bool $valid,
private ErrorMessages $errorMessages,
) {
}

public static function pass(
int|string $name,
mixed $rawValue,
mixed $value,
): self {
return new self($name, $rawValue, $value, true, new ErrorMessages([]));
}

public static function fail(
int|string $name,
mixed $rawValue,
mixed $value,
ErrorMessages $errorMessages,
): self {
return new self($name, $rawValue, $value, false, $errorMessages);
}

public function name(): int|string
{
return $this->name;
}

public function valid(): bool
{
return $this->valid;
}

public function getMessages(): ErrorMessages
{
return $this->errorMessages;
}

public function rawValue(): mixed
{
return $this->rawValue;
}

public function value(): mixed
{
return $this->value;
}
}
16 changes: 16 additions & 0 deletions src/ValidationResultInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Laminas\InputFilter;

interface ValidationResultInterface
{
public function valid(): bool;

public function getMessages(): ErrorMessages;

public function rawValue(): mixed;

public function value(): mixed;
}
Loading
Loading