diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 4d9f3d8..3d3e015 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -76,14 +76,14 @@ - + - + - + - + diff --git a/src/ConfigProvider.php b/src/ConfigProvider.php index 3e79a3b..cb27092 100644 --- a/src/ConfigProvider.php +++ b/src/ConfigProvider.php @@ -23,8 +23,6 @@ use Kynx\Laminas\FormShape\Form\FormVisitorFactory; use Kynx\Laminas\FormShape\InputFilter\ArrayInputVisitor; use Kynx\Laminas\FormShape\InputFilter\ArrayInputVisitorFactory; -use Kynx\Laminas\FormShape\InputFilter\CollectionInputVisitor; -use Kynx\Laminas\FormShape\InputFilter\CollectionInputVisitorFactory; use Kynx\Laminas\FormShape\InputFilter\InputFilterVisitor; use Kynx\Laminas\FormShape\InputFilter\InputFilterVisitorFactory; use Kynx\Laminas\FormShape\InputFilter\InputVisitor; @@ -158,7 +156,6 @@ private function getLaminasFormShapeConfig(): array ], 'input-visitors' => [ ArrayInputVisitor::class, - CollectionInputVisitor::class, InputVisitor::class, ], 'filter' => [ @@ -222,24 +219,23 @@ private function getDependencyConfig(): array TypeNamerInterface::class => TypeNamer::class, ], 'factories' => [ - AllowListVisitor::class => AllowListVisitorFactory::class, - ArrayInputVisitor::class => ArrayInputVisitorFactory::class, - CollectionInputVisitor::class => CollectionInputVisitorFactory::class, - ExplodeVisitor::class => ExplodeVisitorFactory::class, - FileValidatorVisitor::class => FileValidatorVisitorFactory::class, - FormLocator::class => FormLocatorFactory::class, - FormProcessor::class => FormProcessorFactory::class, - NetteCodeGenerator::class => NetteCodeGeneratorFactory::class, - PrettyPrinter::class => PrettyPrinterFactory::class, - PsalmTypeCommand::class => PsalmTypeCommandFactory::class, - FileWriter::class => FileWriterFactory::class, - FormVisitor::class => FormVisitorFactory::class, - InArrayVisitor::class => InArrayVisitorFactory::class, - InputFilterVisitor::class => InputFilterVisitorFactory::class, - InputVisitor::class => InputVisitorFactory::class, - NonEmptyStringVisitor::class => NonEmptyStringVisitorFactory::class, - RegexVisitor::class => RegexVisitorFactory::class, - TypeNamer::class => TypeNamerFactory::class, + AllowListVisitor::class => AllowListVisitorFactory::class, + ArrayInputVisitor::class => ArrayInputVisitorFactory::class, + ExplodeVisitor::class => ExplodeVisitorFactory::class, + FileValidatorVisitor::class => FileValidatorVisitorFactory::class, + FormLocator::class => FormLocatorFactory::class, + FormProcessor::class => FormProcessorFactory::class, + NetteCodeGenerator::class => NetteCodeGeneratorFactory::class, + PrettyPrinter::class => PrettyPrinterFactory::class, + PsalmTypeCommand::class => PsalmTypeCommandFactory::class, + FileWriter::class => FileWriterFactory::class, + FormVisitor::class => FormVisitorFactory::class, + InArrayVisitor::class => InArrayVisitorFactory::class, + InputFilterVisitor::class => InputFilterVisitorFactory::class, + InputVisitor::class => InputVisitorFactory::class, + NonEmptyStringVisitor::class => NonEmptyStringVisitorFactory::class, + RegexVisitor::class => RegexVisitorFactory::class, + TypeNamer::class => TypeNamerFactory::class, ], ]; } diff --git a/src/Form/FormVisitor.php b/src/Form/FormVisitor.php index b9cc25e..67e76b4 100644 --- a/src/Form/FormVisitor.php +++ b/src/Form/FormVisitor.php @@ -143,7 +143,12 @@ private function convertCollectionFilters( $count = $required ? $elementOrFieldset->getCount() : 0; if ($target instanceof InputInterface) { - $inputOrFilter = CollectionInput::fromInput($target, $count); + /** @psalm-suppress TooFewArguments It thinks `getRawValue()` takes an argument ?! */ + $target->setValue([$target->getRawValue()]); + + $inputOrFilter = new CollectionInput($target->getName()); + $inputOrFilter->merge($target); + $inputOrFilter->setCount($count); } else { $inputOrFilter = new CollectionInputFilter(); $inputOrFilter->setIsRequired($required); diff --git a/src/InputFilter/AbstractInputVisitor.php b/src/InputFilter/AbstractInputVisitor.php new file mode 100644 index 0000000..0067042 --- /dev/null +++ b/src/InputFilter/AbstractInputVisitor.php @@ -0,0 +1,116 @@ + $filterVisitors + * @param array $validatorVisitors + */ + public function __construct(protected array $filterVisitors, protected array $validatorVisitors) + { + } + + protected function visitInput(InputInterface $input, Union $initial): Union + { + $union = $initial->getBuilder()->freeze(); + + foreach ($input->getFilterChain()->getIterator() as $filter) { + if (! $filter instanceof FilterInterface) { + continue; + } + $union = $this->visitFilters($filter, $union); + } + + $validators = $this->prependNotEmptyValidator($input, array_map( + static fn (array $queueItem): ValidatorInterface => $queueItem['instance'], + $input->getValidatorChain()->getValidators() + )); + + foreach ($validators as $validator) { + $union = $this->visitValidators($validator, $union); + } + + if (! $this->continueIfEmpty($input) && ($input->allowEmpty() || ! $input->isRequired())) { + $union = TypeUtil::widen($union, $initial); + } + + return $union; + } + + /** + * @psalm-assert-if-true Input $input + */ + protected function hasFallback(InputInterface $input): bool + { + return $input instanceof Input && $input->hasFallback(); + } + + private function continueIfEmpty(InputInterface $input): bool + { + return $input instanceof EmptyContextInterface && $input->continueIfEmpty(); + } + + /** + * @param array $validators + * @return array + */ + private function prependNotEmptyValidator(InputInterface $input, array $validators): array + { + $hasNotEmpty = (bool) array_filter( + $validators, + static fn (ValidatorInterface $validator): bool => $validator instanceof NotEmpty + ); + if ($hasNotEmpty) { + return $validators; + } + + /** + * There's some weirdness here: on the default `Text` element, upstream `''` validates, but `' '` fails. So + * while I _think_ this should be `! $continueIfEmpty && ($input->isRequired() || ! $input->allowEmpty())`, it + * can't be. And I shudder to think of the mayhem it would cause if I raised it as a bug ;) + */ + if (! $this->continueIfEmpty($input) && $input->isRequired() && ! $input->allowEmpty()) { + array_unshift($validators, new NotEmpty()); + } + + return $validators; + } + + private function visitFilters(FilterInterface $filter, Union $union): Union + { + foreach ($this->filterVisitors as $visitor) { + $union = $visitor->visit($filter, $union); + } + + return $union; + } + + private function visitValidators(ValidatorInterface $validator, Union $union): Union + { + foreach ($this->validatorVisitors as $visitor) { + $union = $visitor->visit($validator, $union); + } + + return $union; + } +} diff --git a/src/InputFilter/AbstractInputVisitorFactory.php b/src/InputFilter/AbstractInputVisitorFactory.php new file mode 100644 index 0000000..ca6ed95 --- /dev/null +++ b/src/InputFilter/AbstractInputVisitorFactory.php @@ -0,0 +1,68 @@ + + */ + protected function getFilterVisitors(ContainerInterface $container): array + { + /** @var FormShapeConfigurationArray $config */ + $config = $container->get('config') ?? []; + + $filterVisitors = []; + foreach ($config['laminas-form-shape']['filter-visitors'] as $visitorName) { + $visitor = $this->getVisitor($container, $visitorName); + $filterVisitors[] = $visitor; + } + + return $filterVisitors; + } + + /** + * @return array + */ + protected function getValidatorVisitors(ContainerInterface $container): array + { + /** @var FormShapeConfigurationArray $config */ + $config = $container->get('config') ?? []; + + $validatorVisitors = []; + foreach ($config['laminas-form-shape']['validator-visitors'] as $visitorName) { + $visitor = $this->getVisitor($container, $visitorName); + $validatorVisitors[] = $visitor; + } + + return $validatorVisitors; + } + + /** + * @template T of FilterVisitorInterface|ValidatorVisitorInterface + * @param class-string $visitorName + * @return T + */ + private function getVisitor( + ContainerInterface $container, + string $visitorName + ): FilterVisitorInterface|ValidatorVisitorInterface { + if ($container->has($visitorName)) { + return $container->get($visitorName); + } + + return new $visitorName(); + } +} diff --git a/src/InputFilter/ArrayInputVisitor.php b/src/InputFilter/ArrayInputVisitor.php index c0bac49..5c51d0f 100644 --- a/src/InputFilter/ArrayInputVisitor.php +++ b/src/InputFilter/ArrayInputVisitor.php @@ -4,32 +4,58 @@ namespace Kynx\Laminas\FormShape\InputFilter; -use Kynx\Laminas\FormShape\InputVisitorInterface; +use Kynx\Laminas\FormShape\Psalm\TypeUtil; use Laminas\InputFilter\ArrayInput; use Laminas\InputFilter\InputInterface; use Psalm\Type; +use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TNonEmptyArray; +use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\Union; -final readonly class ArrayInputVisitor implements InputVisitorInterface -{ - public function __construct(private InputVisitor $inputVisitor) - { - } +use function array_map; +final readonly class ArrayInputVisitor extends AbstractInputVisitor +{ public function visit(InputInterface $input): ?Union { - if (! ($input instanceof ArrayInput || $input instanceof CollectionInput)) { + if (! $input instanceof ArrayInput) { return null; } - // @fixme Is the logic here exactly the same? - $union = $this->inputVisitor->visit($input); - $array = $input->isRequired() - ? new TNonEmptyArray([Type::getArrayKey(), new Union($union->getAtomicTypes())]) - : new TArray([Type::getArrayKey(), new Union($union->getAtomicTypes())]); + $initial = new Union([new TNull(), new TString()]); + $union = $this->visitInput($input, $initial); - return new Union([$array], ['possibly_undefined' => $union->possibly_undefined]); + if ($union->getAtomicTypes() === []) { + throw InputVisitorException::cannotGetInputType($input); + } + + $union = new Union([new TArray([Type::getArrayKey(), $union])]); + + if ($this->isNonEmpty($input)) { + $nonEmpty = array_map( + static fn (Atomic $type): Atomic => $type instanceof TArray + ? new TNonEmptyArray($type->type_params) + : $type, + $union->getAtomicTypes() + ); + $union = new Union($nonEmpty); + } + + if ($this->hasFallback($input)) { + return Type::combineUnionTypes($union, TypeUtil::toStrictUnion($input->getFallbackValue())); + } + + return $union; + } + + private function isNonEmpty(ArrayInput $input): bool + { + if ($input instanceof CollectionInput) { + return (bool) $input->getCount(); + } + return $input->isRequired(); } } diff --git a/src/InputFilter/ArrayInputVisitorFactory.php b/src/InputFilter/ArrayInputVisitorFactory.php index 42dc53e..294bb4b 100644 --- a/src/InputFilter/ArrayInputVisitorFactory.php +++ b/src/InputFilter/ArrayInputVisitorFactory.php @@ -6,10 +6,13 @@ use Psr\Container\ContainerInterface; -final readonly class ArrayInputVisitorFactory +final readonly class ArrayInputVisitorFactory extends AbstractInputVisitorFactory { public function __invoke(ContainerInterface $container): ArrayInputVisitor { - return new ArrayInputVisitor($container->get(InputVisitor::class)); + return new ArrayInputVisitor( + $this->getFilterVisitors($container), + $this->getValidatorVisitors($container) + ); } } diff --git a/src/InputFilter/CollectionInput.php b/src/InputFilter/CollectionInput.php index ac447e9..f699001 100644 --- a/src/InputFilter/CollectionInput.php +++ b/src/InputFilter/CollectionInput.php @@ -4,190 +4,28 @@ namespace Kynx\Laminas\FormShape\InputFilter; -use Laminas\Filter\FilterChain; -use Laminas\InputFilter\EmptyContextInterface; -use Laminas\InputFilter\InputInterface; -use Laminas\Validator\ValidatorChain; - -use function assert; -use function is_array; +use Laminas\InputFilter\ArrayInput; /** - * Input for handling form collections where the target element is an actual element, not a fieldset - * - * This is not designed for real-world validation; it's purpose is to capture the `count` and (very probably) - * `possibly_undefined` state of the collection, but otherwise proxy the element's actual `InputInterface`. + * Specialised input for representing collections with non-fieldset target elements * * @internal * - * @see CollectionInputVisitor - * * @psalm-internal Kynx\Laminas\FormShape * @psalm-internal KynxTest\Laminas\FormShape */ -final readonly class CollectionInput implements InputInterface, EmptyContextInterface +final class CollectionInput extends ArrayInput { - private function __construct(private InputInterface $delegate, private int $count) - { - } + private int $count = 0; - public static function fromInput(InputInterface $input, int $count): self + public function setCount(int $count): self { - return new self($input, $count); + $this->count = $count; + return $this; } public function getCount(): int { return $this->count; } - - /** - * @param bool $continueIfEmpty - */ - public function setContinueIfEmpty($continueIfEmpty): self - { - if ($this->delegate instanceof EmptyContextInterface) { - $this->delegate->setContinueIfEmpty($continueIfEmpty); - } - return $this; - } - - public function continueIfEmpty(): bool - { - if ($this->delegate instanceof EmptyContextInterface) { - return $this->delegate->continueIfEmpty(); - } - return false; - } - - /** - * @param bool $allowEmpty - */ - public function setAllowEmpty($allowEmpty): self - { - $this->delegate->setAllowEmpty($allowEmpty); - return $this; - } - - /** - * @param bool $breakOnFailure - */ - public function setBreakOnFailure($breakOnFailure): self - { - $this->delegate->setBreakOnFailure($breakOnFailure); - return $this; - } - - /** - * @param null|string $errorMessage - */ - public function setErrorMessage($errorMessage): self - { - $this->delegate->setErrorMessage($errorMessage); - return $this; - } - - public function setFilterChain(FilterChain $filterChain): self - { - $this->delegate->setFilterChain($filterChain); - return $this; - } - - /** - * @param string $name - */ - public function setName($name): self - { - $this->delegate->setName($name); - return $this; - } - - /** - * @param bool $required - */ - public function setRequired($required): self - { - $this->delegate->setRequired($required); - return $this; - } - - public function setValidatorChain(ValidatorChain $validatorChain): self - { - $this->delegate->setValidatorChain($validatorChain); - return $this; - } - - /** - * @param mixed $value - */ - public function setValue($value): self - { - assert(is_array($value)); - $this->delegate->setValue($value); - return $this; - } - - public function merge(InputInterface $input): self - { - $this->delegate->merge($input); - return $this; - } - - public function allowEmpty(): bool - { - return $this->delegate->allowEmpty(); - } - - public function breakOnFailure(): bool - { - return $this->delegate->breakOnFailure(); - } - - public function getErrorMessage(): ?string - { - return $this->delegate->getErrorMessage(); - } - - public function getFilterChain(): FilterChain - { - return $this->delegate->getFilterChain(); - } - - public function getName(): string - { - return $this->delegate->getName(); - } - - public function getRawValue(): mixed - { - return $this->delegate->getRawValue(); - } - - public function isRequired(): bool - { - return $this->delegate->isRequired(); - } - - public function getValidatorChain(): ValidatorChain - { - return $this->delegate->getValidatorChain(); - } - - public function getValue(): mixed - { - return $this->delegate->getValue(); - } - - /** - * @param mixed $context - */ - public function isValid($context = null): bool - { - return $this->delegate->isValid(); - } - - public function getMessages(): array - { - return $this->delegate->getMessages(); - } } diff --git a/src/InputFilter/CollectionInputVisitor.php b/src/InputFilter/CollectionInputVisitor.php deleted file mode 100644 index 773150e..0000000 --- a/src/InputFilter/CollectionInputVisitor.php +++ /dev/null @@ -1,33 +0,0 @@ -inputVisitor->visit($input); - $array = $input->getCount() > 0 - ? new TNonEmptyArray([Type::getArrayKey(), new Union($union->getAtomicTypes())]) - : new TArray([Type::getArrayKey(), new Union($union->getAtomicTypes())]); - - return new Union([$array]); - } -} diff --git a/src/InputFilter/CollectionInputVisitorFactory.php b/src/InputFilter/CollectionInputVisitorFactory.php deleted file mode 100644 index 09a2e07..0000000 --- a/src/InputFilter/CollectionInputVisitorFactory.php +++ /dev/null @@ -1,15 +0,0 @@ -get(InputVisitor::class)); - } -} diff --git a/src/InputFilter/InputVisitor.php b/src/InputFilter/InputVisitor.php index 385b649..c509310 100644 --- a/src/InputFilter/InputVisitor.php +++ b/src/InputFilter/InputVisitor.php @@ -4,71 +4,22 @@ namespace Kynx\Laminas\FormShape\InputFilter; -use Kynx\Laminas\FormShape\FilterVisitorInterface; use Kynx\Laminas\FormShape\InputFilter\InputVisitorException; -use Kynx\Laminas\FormShape\InputVisitorInterface; use Kynx\Laminas\FormShape\Psalm\TypeUtil; -use Kynx\Laminas\FormShape\ValidatorVisitorInterface; -use Laminas\Filter\FilterInterface; -use Laminas\InputFilter\EmptyContextInterface; -use Laminas\InputFilter\Input; use Laminas\InputFilter\InputInterface; -use Laminas\Validator\NotEmpty; -use Laminas\Validator\ValidatorInterface; use Psalm\Type; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TString; use Psalm\Type\Union; -use function array_map; -use function array_unshift; - -final readonly class InputVisitor implements InputVisitorInterface +final readonly class InputVisitor extends AbstractInputVisitor { - /** - * @param array $filterVisitors - * @param array $validatorVisitors - */ - public function __construct(private array $filterVisitors, private array $validatorVisitors) - { - } - public function visit(InputInterface $input): Union { - $hasFallback = $input instanceof Input && $input->hasFallback(); - $union = new Union([new TNull(), new TString()]); - - foreach ($input->getFilterChain()->getIterator() as $filter) { - if (! $filter instanceof FilterInterface) { - continue; - } - $union = $this->visitFilters($filter, $union); - } - - $validators = array_map( - static fn (array $queueItem): ValidatorInterface => $queueItem['instance'], - $input->getValidatorChain()->getValidators() - ); - - $continueIfEmpty = $input instanceof EmptyContextInterface && $input->continueIfEmpty(); - /** - * There's some weirdness here: on the default `Text` element, upstream `''` validates, but `' '` fails. So - * while I _think_ this should be `! $continueIfEmpty && ($input->isRequired() || ! $input->allowEmpty())`, it - * can't be. And I shudder to think of the mayhem it would cause if I raised it as a bug ;) - */ - if (! $continueIfEmpty && $input->isRequired() && ! $input->allowEmpty()) { - array_unshift($validators, new NotEmpty()); - } - - foreach ($validators as $validator) { - $union = $this->visitValidators($validator, $union); - } - - if (! $continueIfEmpty && ($input->allowEmpty() || ! $input->isRequired())) { - $union = TypeUtil::widen($union, new Union([new TString(), new TNull()])); - } + $initial = new Union([new TNull(), new TString()]); + $union = $this->visitInput($input, $initial); - if ($input instanceof Input && $hasFallback) { + if ($this->hasFallback($input)) { $union = Type::combineUnionTypes($union, TypeUtil::toStrictUnion($input->getFallbackValue())); } @@ -78,22 +29,4 @@ public function visit(InputInterface $input): Union return $union; } - - private function visitFilters(FilterInterface $filter, Union $union): Union - { - foreach ($this->filterVisitors as $visitor) { - $union = $visitor->visit($filter, $union); - } - - return $union; - } - - private function visitValidators(ValidatorInterface $validator, Union $union): Union - { - foreach ($this->validatorVisitors as $visitor) { - $union = $visitor->visit($validator, $union); - } - - return $union; - } } diff --git a/src/InputFilter/InputVisitorFactory.php b/src/InputFilter/InputVisitorFactory.php index 1220439..56e670f 100644 --- a/src/InputFilter/InputVisitorFactory.php +++ b/src/InputFilter/InputVisitorFactory.php @@ -4,52 +4,15 @@ namespace Kynx\Laminas\FormShape\InputFilter; -use Kynx\Laminas\FormShape\ConfigProvider; -use Kynx\Laminas\FormShape\FilterVisitorInterface; -use Kynx\Laminas\FormShape\InputFilter\InputVisitor; -use Kynx\Laminas\FormShape\ValidatorVisitorInterface; use Psr\Container\ContainerInterface; -use function assert; - -/** - * @psalm-import-type FormShapeConfigurationArray from ConfigProvider - */ -final readonly class InputVisitorFactory +final readonly class InputVisitorFactory extends AbstractInputVisitorFactory { public function __invoke(ContainerInterface $container): InputVisitor { - /** @var FormShapeConfigurationArray $config */ - $config = $container->get('config') ?? []; - - $filterVisitors = []; - foreach ($config['laminas-form-shape']['filter-visitors'] as $visitorName) { - $visitor = $this->getVisitor($container, $visitorName); - assert($visitor instanceof FilterVisitorInterface); - $filterVisitors[] = $visitor; - } - - $validatorVisitors = []; - foreach ($config['laminas-form-shape']['validator-visitors'] as $visitorName) { - $visitor = $this->getVisitor($container, $visitorName); - assert($visitor instanceof ValidatorVisitorInterface); - $validatorVisitors[] = $visitor; - } - - return new InputVisitor($filterVisitors, $validatorVisitors); - } - - /** - * @param class-string $visitorName - */ - private function getVisitor( - ContainerInterface $container, - string $visitorName - ): FilterVisitorInterface|ValidatorVisitorInterface { - if ($container->has($visitorName)) { - return $container->get($visitorName); - } - - return new $visitorName(); + return new InputVisitor( + $this->getFilterVisitors($container), + $this->getValidatorVisitors($container) + ); } } diff --git a/test/Form/FormCollectionSmokeTest.php b/test/Form/FormCollectionSmokeTest.php index c748daf..b6fe330 100644 --- a/test/Form/FormCollectionSmokeTest.php +++ b/test/Form/FormCollectionSmokeTest.php @@ -6,7 +6,7 @@ use Kynx\Laminas\FormShape\Decorator\PrettyPrinter; use Kynx\Laminas\FormShape\Form\FormVisitor; -use Kynx\Laminas\FormShape\InputFilter\CollectionInputVisitor; +use Kynx\Laminas\FormShape\InputFilter\ArrayInputVisitor; use Kynx\Laminas\FormShape\InputFilter\InputFilterVisitor; use Kynx\Laminas\FormShape\InputFilter\InputVisitor; use Kynx\Laminas\FormShape\Psalm\ConfigLoader; @@ -34,10 +34,10 @@ protected function setUp(): void { parent::setUp(); - $inputVisitor = new InputVisitor([], [new NotEmptyVisitor()]); - $collectionInputVisitor = new CollectionInputVisitor($inputVisitor); - $inputFilterVisitor = new InputFilterVisitor([ - $collectionInputVisitor, + $inputVisitor = new InputVisitor([], [new NotEmptyVisitor()]); + $arrayInputVisitor = new ArrayInputVisitor([], [new NotEmptyVisitor()]); + $inputFilterVisitor = new InputFilterVisitor([ + $arrayInputVisitor, $inputVisitor, ]); diff --git a/test/Form/FormElementSmokeTest.php b/test/Form/FormElementSmokeTest.php index c465514..cdc2c63 100644 --- a/test/Form/FormElementSmokeTest.php +++ b/test/Form/FormElementSmokeTest.php @@ -239,4 +239,21 @@ public static function defaultElementProvider(): array ], ]; } + + public function testMultiCheckboxValidatesSingleString(): void + { + $form = new Form(); + $multiCheckbox = new MultiCheckbox('foo', ['value_options' => [1 => 'a', 2 => 'b']]); + $form->add($multiCheckbox); + + $form->setData(['foo' => '1']); + $isValid = $form->isValid(); + self::assertTrue($isValid); + $data = $form->getData(); + self::assertSame(['foo' => '1'], $data); + + $form->setData(['foo' => ['1', '2']]); + $isValid = $form->isValid(); + self::assertTrue($isValid); + } } diff --git a/test/Form/FormFieldsetSmokeTest.php b/test/Form/FormFieldsetSmokeTest.php index 56f6ec6..a7a24fd 100644 --- a/test/Form/FormFieldsetSmokeTest.php +++ b/test/Form/FormFieldsetSmokeTest.php @@ -6,7 +6,7 @@ use Kynx\Laminas\FormShape\Decorator\PrettyPrinter; use Kynx\Laminas\FormShape\Form\FormVisitor; -use Kynx\Laminas\FormShape\InputFilter\CollectionInputVisitor; +use Kynx\Laminas\FormShape\InputFilter\ArrayInputVisitor; use Kynx\Laminas\FormShape\InputFilter\InputFilterVisitor; use Kynx\Laminas\FormShape\InputFilter\InputVisitor; use Kynx\Laminas\FormShape\Psalm\ConfigLoader; @@ -32,10 +32,10 @@ protected function setUp(): void { parent::setUp(); - $inputVisitor = new InputVisitor([], [new NotEmptyVisitor()]); - $collectionInputVisitor = new CollectionInputVisitor($inputVisitor); - $inputFilterVisitor = new InputFilterVisitor([ - $collectionInputVisitor, + $inputVisitor = new InputVisitor([], [new NotEmptyVisitor()]); + $arrayInputVisitor = new ArrayInputVisitor([], [new NotEmptyVisitor()]); + $inputFilterVisitor = new InputFilterVisitor([ + $arrayInputVisitor, $inputVisitor, ]); diff --git a/test/Form/FormProcessorTest.php b/test/Form/FormProcessorTest.php index 5996c8e..89e9b65 100644 --- a/test/Form/FormProcessorTest.php +++ b/test/Form/FormProcessorTest.php @@ -6,7 +6,7 @@ use Kynx\Laminas\FormShape\Form\FormProcessor; use Kynx\Laminas\FormShape\Form\FormVisitor; -use Kynx\Laminas\FormShape\InputFilter\CollectionInputVisitor; +use Kynx\Laminas\FormShape\InputFilter\ArrayInputVisitor; use Kynx\Laminas\FormShape\InputFilter\ImportType; use Kynx\Laminas\FormShape\InputFilter\InputFilterVisitor; use Kynx\Laminas\FormShape\InputFilter\InputVisitor; @@ -52,11 +52,11 @@ protected function setUp(): void $this->listener = new MockProgressListener(); $inputVisitor = new InputVisitor([], []); - $collectionVisitor = new CollectionInputVisitor($inputVisitor); + $arrayInputVisitor = new ArrayInputVisitor([], []); $this->processor = new FormProcessor( $this->formLocator, - new FormVisitor(new InputFilterVisitor([$collectionVisitor, $inputVisitor])), + new FormVisitor(new InputFilterVisitor([$arrayInputVisitor, $inputVisitor])), $this->fileWriter ); } diff --git a/test/Form/FormVisitorTest.php b/test/Form/FormVisitorTest.php index 33f490f..a3f09f9 100644 --- a/test/Form/FormVisitorTest.php +++ b/test/Form/FormVisitorTest.php @@ -5,10 +5,11 @@ namespace KynxTest\Laminas\FormShape\Form; use Kynx\Laminas\FormShape\Form\FormVisitor; -use Kynx\Laminas\FormShape\InputFilter\CollectionInputVisitor; +use Kynx\Laminas\FormShape\InputFilter\ArrayInputVisitor; use Kynx\Laminas\FormShape\InputFilter\ImportType; use Kynx\Laminas\FormShape\InputFilter\InputFilterVisitor; use Kynx\Laminas\FormShape\InputFilter\InputVisitor; +use Kynx\Laminas\FormShape\Psalm\ConfigLoader; use KynxTest\Laminas\FormShape\Form\Asset\InputFilterFieldset; use Laminas\Form\Element\Collection; use Laminas\Form\Element\Email; @@ -39,9 +40,11 @@ protected function setUp(): void { parent::setUp(); + ConfigLoader::load(); + $inputVisitor = new InputVisitor([], []); - $collectionVisitor = new CollectionInputVisitor($inputVisitor); - $this->visitor = new FormVisitor(new InputFilterVisitor([$collectionVisitor, $inputVisitor])); + $arrayInputVisitor = new ArrayInputVisitor([], []); + $this->visitor = new FormVisitor(new InputFilterVisitor([$arrayInputVisitor, $inputVisitor])); } public function testVisitSingleElement(): void diff --git a/test/InputFilter/AbstractInputVisitorFactoryTest.php b/test/InputFilter/AbstractInputVisitorFactoryTest.php new file mode 100644 index 0000000..b045090 --- /dev/null +++ b/test/InputFilter/AbstractInputVisitorFactoryTest.php @@ -0,0 +1,45 @@ +createStub(ContainerInterface::class); + $container->method('get') + ->willReturnMap([ + ['config', $this->getConfig([AllowListVisitor::class], [BetweenVisitor::class])], + [AllowListVisitor::class, $allowListVisitor], + [BetweenVisitor::class, $betweenVisitor], + ]); + + $factory = new MockAbstractInputVisitorFactory(); + $instance = $factory($container); + + self::assertEquals([$allowListVisitor], $instance->getFilterVisitors()); + self::assertEquals([$betweenVisitor], $instance->getValidatorVisitors()); + } + + private function getConfig(array $filterVisitors, array $validatorVisitors): array + { + return [ + 'laminas-form-shape' => [ + 'filter-visitors' => $filterVisitors, + 'validator-visitors' => $validatorVisitors, + ], + ]; + } +} diff --git a/test/InputFilter/AbstractInputVisitorTest.php b/test/InputFilter/AbstractInputVisitorTest.php new file mode 100644 index 0000000..a9daf30 --- /dev/null +++ b/test/InputFilter/AbstractInputVisitorTest.php @@ -0,0 +1,146 @@ +getFilterChain()->attach(new ToInt()); + $visitor = new MockAbstractInputVisitor([new ToIntVisitor()], []); + + $actual = $visitor->visit($input); + self::assertEquals($expected, $actual); + } + + public function testVisitSkipsCallableFilters(): void + { + $expected = new Union([new TNull(), new TString()]); + $filter = static fn (): never => self::fail("Should not be called"); + $input = new Input('foo'); + $input->getFilterChain()->attach($filter); + $visitor = new MockAbstractInputVisitor([new ToIntVisitor()], []); + + $actual = $visitor->visit($input); + self::assertEquals($expected, $actual); + } + + public function testVisitCallsValidator(): void + { + $expected = new Union([new TNumericString()]); + $input = new Input('foo'); + $input->getValidatorChain()->attach(new Digits()); + $visitor = new MockAbstractInputVisitor([], [new DigitsVisitor()]); + + $actual = $visitor->visit($input); + self::assertEquals($expected, $actual); + } + + /** + * @param non-empty-array $expected + */ + #[DataProvider('addNotEmptyProvider')] + public function testVisitAddsNotEmptyValidator( + bool $continueIfEmpty, + bool $allowEmpty, + bool $required, + array $expected + ): void { + $expected = new Union($expected); + $input = new Input('foo'); + $input->setContinueIfEmpty($continueIfEmpty); + $input->setAllowEmpty($allowEmpty); + $input->setRequired($required); + $visitor = new MockAbstractInputVisitor([], [new NotEmptyVisitor()]); + + $actual = $visitor->visit($input); + + self::assertEquals($expected, $actual); + } + + public static function addNotEmptyProvider(): array + { + ConfigLoader::load(); + + // phpcs:disable Generic.Files.LineLength.TooLong + return [ + "continue, allow, required" => [true, true, true, [new TString(), new TNull()]], + "continue, allow, not required" => [true, true, false, [new TString(), new TNull()]], + "continue, don't allow, required" => [true, false, true, [new TString(), new TNull()]], + "continue, don't allow, not required" => [true, false, false, [new TString(), new TNull()]], + "don't continue, allow, required" => [false, true, true, [new TNull(), new TString()]], + "don't continue, allow, not required" => [false, true, false, [new TNull(), new TString()]], + "don't continue, don't allow, required" => [false, false, true, [new TNonEmptyString()]], + "don't continue, don't allow, not required" => [false, false, false, [new TNull(), new TString()]], + ]; + // phpcs:enable + } + + public function testVisitInputDoesNotPrependDuplicateNotEmptyVisitor(): void + { + ConfigLoader::load(); + + $input = new Input('foo'); + $input->setContinueIfEmpty(false); + $input->setAllowEmpty(false); + $input->setRequired(true); + $input->getValidatorChain()->attach(new NotEmpty()); + + $mockVisitor = self::createMock(ValidatorVisitorInterface::class); + $mockVisitor->method('visit') + ->willReturnCallback(static function (ValidatorInterface $validator, Union $previous) { + self::assertNotEquals(new Union([new TNonEmptyString()]), $previous); + return $previous; + }); + + $visitor = new MockAbstractInputVisitor([], [$mockVisitor, new NotEmptyVisitor()]); + $actual = $visitor->visit($input); + self::assertNotNull($actual); + self::assertEquals(new TNonEmptyString(), $actual->getSingleAtomic()); + } + + public function testVisitAllowEmptyReplacesNotEmptyString(): void + { + $expected = new Union([new TString(), new TNull()]); + $validatorVisitor = $this->createMock(ValidatorVisitorInterface::class); + $validatorVisitor->expects(self::once()) + ->method('visit') + ->willReturn(new Union([new TNonEmptyString()])); + $input = new Input('foo'); + $input->setRequired(false); + $input->setAllowEmpty(true); + $input->getValidatorChain()->attach($this->createStub(ValidatorInterface::class)); + $visitor = new MockAbstractInputVisitor([], [$validatorVisitor]); + + $actual = $visitor->visit($input); + self::assertEquals($expected, $actual); + } +} diff --git a/test/InputFilter/ArrayInputVisitorFactoryTest.php b/test/InputFilter/ArrayInputVisitorFactoryTest.php index 6f6a7cf..887c6a3 100644 --- a/test/InputFilter/ArrayInputVisitorFactoryTest.php +++ b/test/InputFilter/ArrayInputVisitorFactoryTest.php @@ -4,12 +4,15 @@ namespace KynxTest\Laminas\FormShape\InputFilter; +use Kynx\Laminas\FormShape\Filter\ToIntVisitor; use Kynx\Laminas\FormShape\InputFilter\ArrayInputVisitorFactory; -use Kynx\Laminas\FormShape\InputFilter\InputVisitor; +use Kynx\Laminas\FormShape\Validator\DigitsVisitor; +use Laminas\Filter\ToInt; use Laminas\InputFilter\ArrayInput; +use Laminas\Validator\Digits; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; -use Psalm\Type\Union; +use Psalm\Type\Atomic\TNonEmptyArray; use Psr\Container\ContainerInterface; #[CoversClass(ArrayInputVisitorFactory::class)] @@ -20,13 +23,29 @@ public function testInvokeReturnsConfiguredInstance(): void $container = $this->createStub(ContainerInterface::class); $container->method('get') ->willReturnMap([ - [InputVisitor::class, new InputVisitor([], [])], + ['config', $this->getConfig([ToIntVisitor::class], [DigitsVisitor::class])], + [ToIntVisitor::class, new ToIntVisitor()], + [DigitsVisitor::class, new DigitsVisitor()], ]); $factory = new ArrayInputVisitorFactory(); $instance = $factory($container); + $input = new ArrayInput(); + $input->getFilterChain()->attach(new ToInt()); + $input->getValidatorChain()->attach(new Digits()); $actual = $instance->visit(new ArrayInput()); - self::assertInstanceOf(Union::class, $actual); + self::assertNotNull($actual); + self::assertInstanceOf(TNonEmptyArray::class, $actual->getSingleAtomic()); + } + + private function getConfig(array $filterVisitors, array $validatorVisitors): array + { + return [ + 'laminas-form-shape' => [ + 'filter-visitors' => $filterVisitors, + 'validator-visitors' => $validatorVisitors, + ], + ]; } } diff --git a/test/InputFilter/ArrayInputVisitorTest.php b/test/InputFilter/ArrayInputVisitorTest.php index 411bb13..5c542a8 100644 --- a/test/InputFilter/ArrayInputVisitorTest.php +++ b/test/InputFilter/ArrayInputVisitorTest.php @@ -4,43 +4,114 @@ namespace KynxTest\Laminas\FormShape\InputFilter; +use Kynx\Laminas\FormShape\Decorator\PrettyPrinter; +use Kynx\Laminas\FormShape\Filter\BooleanVisitor; +use Kynx\Laminas\FormShape\Filter\ToIntVisitor; use Kynx\Laminas\FormShape\InputFilter\ArrayInputVisitor; -use Kynx\Laminas\FormShape\InputFilter\InputVisitor; +use Kynx\Laminas\FormShape\InputFilter\InputVisitorException; +use Kynx\Laminas\FormShape\Psalm\ConfigLoader; +use Kynx\Laminas\FormShape\Psalm\TypeUtil; +use Kynx\Laminas\FormShape\Validator\DigitsVisitor; +use Kynx\Laminas\FormShape\Validator\InArrayVisitor; +use Laminas\Filter\Boolean; +use Laminas\Filter\ToInt; use Laminas\InputFilter\ArrayInput; use Laminas\InputFilter\Input; +use Laminas\Validator\Digits; +use Laminas\Validator\InArray; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Psalm\Type; +use Psalm\Type\Atomic\TArray; +use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TNonEmptyArray; -use Psalm\Type\Atomic\TNull; -use Psalm\Type\Atomic\TString; +use Psalm\Type\Atomic\TNumericString; use Psalm\Type\Union; #[CoversClass(ArrayInputVisitor::class)] final class ArrayInputVisitorTest extends TestCase { - private ArrayInputVisitor $visitor; + public function testVisitNonArrayInputReturnsNull(): void + { + $visitor = new ArrayInputVisitor([], []); + $actual = $visitor->visit(new Input()); + self::assertNull($actual); + } - protected function setUp(): void + public function testVisitImpossibleInputThrowsException(): void { - $this->visitor = new ArrayInputVisitor(new InputVisitor([], [])); + $input = new ArrayInput('foo'); + $input->getFilterChain()->attach(new Boolean()); + $input->getValidatorChain()->attach(new Digits()); + $visitor = new ArrayInputVisitor([new BooleanVisitor()], [new DigitsVisitor()]); + + self::expectException(InputVisitorException::class); + self::expectExceptionMessage("Cannot get type for 'foo'"); + $visitor->visit($input); } - public function testVisitInvalidInputReturnsNull(): void + public function testVisitReturnsArrayOfValidatedType(): void { - $actual = $this->visitor->visit(new Input()); - self::assertNull($actual); + $expected = new Union([ + new TArray([ + Type::getArrayKey(), + new Union([new TNumericString(), new TInt()]), + ]), + ]); + $input = new ArrayInput(); + $input->setRequired(false); + $input->setContinueIfEmpty(true); + $input->getFilterChain()->attach(new ToInt()); + $input->getValidatorChain()->attach(new Digits()); + $visitor = new ArrayInputVisitor([new ToIntVisitor()], [new DigitsVisitor()]); + + $actual = $visitor->visit($input); + self::assertEquals($expected, $actual); } public function testVisitReturnsNonEmptyArray(): void { $expected = new Union([ - new TNonEmptyArray([Type::getArrayKey(), new Union([new TString(), new TNull()])]), + new TNonEmptyArray([ + Type::getArrayKey(), + new Union([new TNumericString(), new TInt()]), + ]), ]); $input = new ArrayInput(); - $input->setRequired(true); + $input->getFilterChain()->attach(new ToInt()); + $input->getValidatorChain()->attach(new Digits()); + $visitor = new ArrayInputVisitor([new ToIntVisitor()], [new DigitsVisitor()]); - $actual = $this->visitor->visit($input); + $actual = $visitor->visit($input); self::assertEquals($expected, $actual); } + + public function testVisitAddsFallbackValue(): void + { + ConfigLoader::load(); + + $expected = new Union([ + new TNonEmptyArray([ + Type::getArrayKey(), + new Union([ + TypeUtil::getAtomicStringFromLiteral('1'), + TypeUtil::getAtomicStringFromLiteral('2'), + TypeUtil::getAtomicStringFromLiteral('a'), + ]), + ]), + ]); + + $input = new ArrayInput(); + $input->getValidatorChain()->attach(new InArray(['haystack' => [1, 2]])); + $input->setFallbackValue(['a']); + $visitor = new ArrayInputVisitor([], [new InArrayVisitor()]); + + $actual = $visitor->visit($input); + + self::assertNotNull($actual); + self::assertEquals($expected, $actual); + + $decorated = (new PrettyPrinter())->decorate($actual); + self::assertSame("non-empty-array", $decorated); + } } diff --git a/test/InputFilter/CollectionInputTest.php b/test/InputFilter/CollectionInputTest.php index 4df9ca0..2bca3e5 100644 --- a/test/InputFilter/CollectionInputTest.php +++ b/test/InputFilter/CollectionInputTest.php @@ -5,51 +5,18 @@ namespace KynxTest\Laminas\FormShape\InputFilter; use Kynx\Laminas\FormShape\InputFilter\CollectionInput; -use Laminas\Filter\FilterChain; -use Laminas\Filter\ToInt; -use Laminas\InputFilter\Input; -use Laminas\Validator\NotEmpty; -use Laminas\Validator\ValidatorChain; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; #[CoversClass(CollectionInput::class)] final class CollectionInputTest extends TestCase { - public function testFromInputSetsProperties(): void + public function testSetCount(): void { - $actual = CollectionInput::fromInput(new Input(), 42); - self::assertSame(42, $actual->getCount()); - } - - public function testFromInputDelegatesProperties(): void - { - $input = new Input(); - $input->setName('foo') - ->setRequired(true) - ->setAllowEmpty(true) - ->setContinueIfEmpty(true) - ->setBreakOnFailure(true) - ->setFilterChain((new FilterChain())->attach(new ToInt())) - ->setValidatorChain((new ValidatorChain())->attach(new NotEmpty())); - - $actual = CollectionInput::fromInput($input, 0); - - self::assertSame($input->getName(), $actual->getName()); - self::assertSame($input->isRequired(), $actual->isRequired()); - self::assertSame($input->allowEmpty(), $actual->allowEmpty()); - self::assertSame($input->continueIfEmpty(), $actual->continueIfEmpty()); - self::assertSame($input->breakOnFailure(), $actual->breakOnFailure()); - self::assertSame($input->getFilterChain(), $actual->getFilterChain()); - self::assertSame($input->getValidatorChain(), $actual->getValidatorChain()); - } - - public function testIsValidDelegates(): void - { - $input = CollectionInput::fromInput((new Input())->setRequired(true), 0); - $input->setValue([]); - - $actual = $input->isValid(); - self::assertFalse($actual); + $expected = 1; + $input = new CollectionInput(); + $input->setCount($expected); + $actual = $input->getCount(); + self::assertSame($expected, $actual); } } diff --git a/test/InputFilter/CollectionInputVisitorFactoryTest.php b/test/InputFilter/CollectionInputVisitorFactoryTest.php deleted file mode 100644 index 0fcd24c..0000000 --- a/test/InputFilter/CollectionInputVisitorFactoryTest.php +++ /dev/null @@ -1,36 +0,0 @@ -createStub(ContainerInterface::class); - $container->method('get') - ->willReturnMap([ - [InputVisitor::class, new InputVisitor([], [])], - ]); - - $factory = new CollectionInputVisitorFactory(); - $instance = $factory($container); - $input = CollectionInput::fromInput(new Input(), 0); - - $union = $instance->visit($input); - self::assertNotNull($union); - $actual = $union->getSingleAtomic(); - self::assertInstanceOf(TArray::class, $actual); - } -} diff --git a/test/InputFilter/CollectionInputVisitorTest.php b/test/InputFilter/CollectionInputVisitorTest.php deleted file mode 100644 index 584be82..0000000 --- a/test/InputFilter/CollectionInputVisitorTest.php +++ /dev/null @@ -1,57 +0,0 @@ -visitor = new CollectionInputVisitor(new InputVisitor([], [])); - } - - public function testVisitInvalidInputReturnsNull(): void - { - $actual = $this->visitor->visit(new Input()); - self::assertNull($actual); - } - - public function testVisitReturnsNonEmptyArray(): void - { - $expected = new Union([ - new TNonEmptyArray([Type::getArrayKey(), new Union([new TString(), new TNull()])]), - ]); - $input = CollectionInput::fromInput(new Input(), 42); - - $actual = $this->visitor->visit($input); - self::assertEquals($expected, $actual); - } - - public function testVisitReturnsArray(): void - { - $expected = new Union([ - new TArray([Type::getArrayKey(), new Union([new TString(), new TNull()])]), - ]); - $input = CollectionInput::fromInput(new Input(), 0); - - $actual = $this->visitor->visit($input); - self::assertEquals($expected, $actual); - } -} diff --git a/test/InputFilter/InputFilterVisitorFactoryTest.php b/test/InputFilter/InputFilterVisitorFactoryTest.php index c0bc97f..8dddb76 100644 --- a/test/InputFilter/InputFilterVisitorFactoryTest.php +++ b/test/InputFilter/InputFilterVisitorFactoryTest.php @@ -50,14 +50,13 @@ public function testInvokeReturnsConfiguredInstance(): void public function testInvokeSortsInputVisitors(): void { - $config = $this->getConfig([InputVisitor::class, ArrayInputVisitor::class]); - $container = self::createStub(ContainerInterface::class); - $inputVisitor = new InputVisitor([], []); + $config = $this->getConfig([InputVisitor::class, ArrayInputVisitor::class]); + $container = self::createStub(ContainerInterface::class); $container->method('get') ->willReturnMap([ ['config', $config], [InputVisitor::class, new InputVisitor([], [])], - [ArrayInputVisitor::class, new ArrayInputVisitor($inputVisitor)], + [ArrayInputVisitor::class, new ArrayInputVisitor([], [])], ]); $factory = new InputFilterVisitorFactory(); diff --git a/test/InputFilter/InputFilterVisitorTest.php b/test/InputFilter/InputFilterVisitorTest.php index a92eb6a..45b0fd4 100644 --- a/test/InputFilter/InputFilterVisitorTest.php +++ b/test/InputFilter/InputFilterVisitorTest.php @@ -308,7 +308,7 @@ public function testVisitReturnsDeeplyNestedImportType(): void public function testVisitNoValidInputVisitorThrowsException(): void { $expected = "No input visitor configured for '" . Input::class . "'"; - $arrayVisitor = new ArrayInputVisitor(new InputVisitor([], [])); + $arrayVisitor = new ArrayInputVisitor([], []); $visitor = new InputFilterVisitor([$arrayVisitor]); $inputFilter = new InputFilter(); $inputFilter->add(new Input()); diff --git a/test/InputFilter/InputVisitorFactoryTest.php b/test/InputFilter/InputVisitorFactoryTest.php index 006f691..6ac8515 100644 --- a/test/InputFilter/InputVisitorFactoryTest.php +++ b/test/InputFilter/InputVisitorFactoryTest.php @@ -7,17 +7,13 @@ use Kynx\Laminas\FormShape\Filter\ToIntVisitor; use Kynx\Laminas\FormShape\InputFilter\InputVisitorFactory; use Kynx\Laminas\FormShape\Validator\DigitsVisitor; -use Kynx\Laminas\FormShape\ValidatorVisitorInterface; use Laminas\Filter\ToInt; use Laminas\InputFilter\Input; use Laminas\Validator\Digits; -use Laminas\Validator\ValidatorInterface; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Psalm\Type\Atomic\TInt; -use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumericString; -use Psalm\Type\Atomic\TString; use Psalm\Type\Union; use Psr\Container\ContainerInterface; @@ -43,34 +39,6 @@ public function testInvokeReturnsConfiguredInstance(): void self::assertEquals($expected, $actual); } - public function testInvokeGetsVisitorFromContainer(): void - { - $config = $this->getConfig([], [ValidatorVisitorInterface::class]); - $validatorVisitor = $this->createMock(ValidatorVisitorInterface::class); - $container = self::createStub(ContainerInterface::class); - $container->method('has') - ->willReturn(true); - $container->method('get') - ->willReturnMap([ - ['config', $config], - [ValidatorVisitorInterface::class, $validatorVisitor], - ]); - - $factory = new InputVisitorFactory(); - $instance = $factory($container); - - $expected = new Union([new TInt(), new TString(), new TNull()]); - $input = new Input('foo'); - $input->setRequired(false); // so we don't attach NotEmpty - $input->getValidatorChain()->attach($this->createStub(ValidatorInterface::class)); - - $validatorVisitor->expects(self::once()) - ->method('visit') - ->willReturn(new Union([new TInt()])); - $actual = $instance->visit($input); - self::assertEquals($expected, $actual); - } - private function getConfig(array $filterVisitors, array $validatorVisitors): array { return [ diff --git a/test/InputFilter/InputVisitorTest.php b/test/InputFilter/InputVisitorTest.php index 985f088..505546d 100644 --- a/test/InputFilter/InputVisitorTest.php +++ b/test/InputFilter/InputVisitorTest.php @@ -5,124 +5,20 @@ namespace KynxTest\Laminas\FormShape\InputFilter; use Kynx\Laminas\FormShape\Filter\BooleanVisitor; -use Kynx\Laminas\FormShape\Filter\ToIntVisitor; use Kynx\Laminas\FormShape\InputFilter\InputVisitor; use Kynx\Laminas\FormShape\InputFilter\InputVisitorException; -use Kynx\Laminas\FormShape\Psalm\ConfigLoader; use Kynx\Laminas\FormShape\Validator\DigitsVisitor; -use Kynx\Laminas\FormShape\Validator\NotEmptyVisitor; -use Kynx\Laminas\FormShape\ValidatorVisitorInterface; use Laminas\Filter\Boolean; -use Laminas\Filter\ToInt; use Laminas\InputFilter\Input; use Laminas\Validator\Digits; -use Laminas\Validator\ValidatorInterface; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; -use Psalm\Type\Atomic; -use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TLiteralFloat; -use Psalm\Type\Atomic\TNonEmptyString; -use Psalm\Type\Atomic\TNull; -use Psalm\Type\Atomic\TNumericString; -use Psalm\Type\Atomic\TString; use Psalm\Type\Union; #[CoversClass(InputVisitor::class)] final class InputVisitorTest extends TestCase { - public function testVisitCallsFilter(): void - { - $expected = new Union([new TNull(), new TString(), new TInt()]); - $input = new Input('foo'); - $input->getFilterChain()->attach(new ToInt()); - $visitor = new InputVisitor([new ToIntVisitor()], []); - - $actual = $visitor->visit($input); - self::assertEquals($expected, $actual); - } - - public function testVisitSkipsCallableFilters(): void - { - $expected = new Union([new TNull(), new TString()]); - $filter = static fn (): never => self::fail("Should not be called"); - $input = new Input('foo'); - $input->getFilterChain()->attach($filter); - $visitor = new InputVisitor([new ToIntVisitor()], []); - - $actual = $visitor->visit($input); - self::assertEquals($expected, $actual); - } - - public function testVisitCallsValidator(): void - { - $expected = new Union([new TNumericString()]); - $input = new Input('foo'); - $input->getValidatorChain()->attach(new Digits()); - $visitor = new InputVisitor([], [new DigitsVisitor()]); - - $actual = $visitor->visit($input); - self::assertEquals($expected, $actual); - } - - /** - * @param non-empty-array $expected - */ - #[DataProvider('addNotEmptyProvider')] - public function testVisitAddsNotEmptyValidator( - bool $continueIfEmpty, - bool $allowEmpty, - bool $required, - array $expected - ): void { - $expected = new Union($expected); - $input = new Input('foo'); - $input->setContinueIfEmpty($continueIfEmpty); - $input->setAllowEmpty($allowEmpty); - $input->setRequired($required); - $visitor = new InputVisitor([], [new NotEmptyVisitor()]); - - $actual = $visitor->visit($input); - - self::assertEquals($expected, $actual); - } - - public static function addNotEmptyProvider(): array - { - ConfigLoader::load(); - - // phpcs:disable Generic.Files.LineLength.TooLong - return [ - "continue, allow, required" => [true, true, true, [new TString(), new TNull()]], - "continue, allow, not required" => [true, true, false, [new TString(), new TNull()]], - "continue, don't allow, required" => [true, false, true, [new TString(), new TNull()]], - "continue, don't allow, not required" => [true, false, false, [new TString(), new TNull()]], - "don't continue, allow, required" => [false, true, true, [new TNull(), new TString()]], - "don't continue, allow, not required" => [false, true, false, [new TNull(), new TString()]], - "don't continue, don't allow, required" => [false, false, true, [new TNonEmptyString()]], - "don't continue, don't allow, not required" => [false, false, false, [new TNull(), new TString()]], - ]; - // phpcs:enable - } - - public function testVisitAllowEmptyReplacesNotEmptyString(): void - { - $expected = new Union([new TString(), new TNull()]); - $validatorVisitor = $this->createMock(ValidatorVisitorInterface::class); - $validatorVisitor->expects(self::once()) - ->method('visit') - ->willReturn(new Union([new TNonEmptyString()])); - $input = new Input('foo'); - $input->setRequired(false); - $input->setAllowEmpty(true); - $input->getValidatorChain()->attach($this->createStub(ValidatorInterface::class)); - $visitor = new InputVisitor([], [$validatorVisitor]); - - $actual = $visitor->visit($input); - self::assertEquals($expected, $actual); - } - public function testVisitAddsFallback(): void { $expected = new Union([new TLiteralFloat(1.23)]); diff --git a/test/InputFilter/MockAbstractInputVisitor.php b/test/InputFilter/MockAbstractInputVisitor.php new file mode 100644 index 0000000..836a2bc --- /dev/null +++ b/test/InputFilter/MockAbstractInputVisitor.php @@ -0,0 +1,19 @@ +visitInput($input, new Union([new TNull(), new TString()])); + } +} diff --git a/test/InputFilter/MockAbstractInputVisitorFactory.php b/test/InputFilter/MockAbstractInputVisitorFactory.php new file mode 100644 index 0000000..67b8530 --- /dev/null +++ b/test/InputFilter/MockAbstractInputVisitorFactory.php @@ -0,0 +1,19 @@ +getFilterVisitors($container), + $this->getValidatorVisitors($container) + ); + } +} diff --git a/test/InputFilter/MockInputVisitor.php b/test/InputFilter/MockInputVisitor.php new file mode 100644 index 0000000..59ab082 --- /dev/null +++ b/test/InputFilter/MockInputVisitor.php @@ -0,0 +1,28 @@ +filterVisitors; + } + + public function getValidatorVisitors(): array + { + return $this->validatorVisitors; + } + + public function visit(InputInterface $input): ?Union + { + return TypeUtil::getEmptyUnion(); + } +}