diff --git a/composer.json b/composer.json index d51f954e..96e66dbd 100644 --- a/composer.json +++ b/composer.json @@ -35,15 +35,15 @@ "laminas/laminas-filter": "^2.19", "laminas/laminas-servicemanager": "^3.21.0", "laminas/laminas-stdlib": "^3.0", - "laminas/laminas-validator": "^2.25" + "laminas/laminas-validator": "^2.52" }, "require-dev": { "ext-json": "*", "laminas/laminas-coding-standard": "~2.5.0", - "phpunit/phpunit": "^10.5.9", + "phpunit/phpunit": "^10.5.15", "psalm/plugin-phpunit": "^0.19.0", "psr/http-message": "^2.0", - "vimeo/psalm": "^5.21.1", + "vimeo/psalm": "^5.23.1", "webmozart/assert": "^1.11" }, "suggest": { diff --git a/composer.lock b/composer.lock index 3b2c7748..9ba1d00c 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7426fb6b7ceaa9a804907b07d312ffb3", + "content-hash": "0393a5f8ad0e360dc3dc490cbd0d525e", "packages": [ { "name": "laminas/laminas-filter", @@ -236,16 +236,16 @@ }, { "name": "laminas/laminas-validator", - "version": "2.51.0", + "version": "2.52.0", "source": { "type": "git", "url": "https://github.com/laminas/laminas-validator.git", - "reference": "25cc42efd096352f4dcfcf55dfb00665a1b29aaf" + "reference": "29087fbe742de8f7adab03969c1b5abff9d88808" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-validator/zipball/25cc42efd096352f4dcfcf55dfb00665a1b29aaf", - "reference": "25cc42efd096352f4dcfcf55dfb00665a1b29aaf", + "url": "https://api.github.com/repos/laminas/laminas-validator/zipball/29087fbe742de8f7adab03969c1b5abff9d88808", + "reference": "29087fbe742de8f7adab03969c1b5abff9d88808", "shasum": "" }, "require": { @@ -262,13 +262,13 @@ "laminas/laminas-db": "^2.19", "laminas/laminas-filter": "^2.34", "laminas/laminas-i18n": "^2.26.0", - "laminas/laminas-session": "^2.18", + "laminas/laminas-session": "^2.20", "laminas/laminas-uri": "^2.11.0", - "phpunit/phpunit": "^10.5.10", - "psalm/plugin-phpunit": "^0.18.4", + "phpunit/phpunit": "^10.5.15", + "psalm/plugin-phpunit": "^0.19.0", "psr/http-client": "^1.0.3", "psr/http-factory": "^1.0.2", - "vimeo/psalm": "^5.22.1" + "vimeo/psalm": "^5.23.1" }, "suggest": { "laminas/laminas-db": "Laminas\\Db component, required by the (No)RecordExists validator", @@ -316,7 +316,7 @@ "type": "community_bridge" } ], - "time": "2024-03-16T03:34:49+00:00" + "time": "2024-03-26T11:05:42+00:00" }, { "name": "psr/container", diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 1b482c6a..afb6fa0d 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,70 +1,58 @@ - + - - is_array($value) - + prepareNotArrayFailureMessage()]]> prepareRequiredValidationFailureMessage()]]> prepareRequiredValidationFailureMessage()]]> - - getFallbackValue()]]> - getFallbackValue()]]> - - $result[$key] - $value - $value + - - $value - $value - - - $value - + + + - $values + - TFilteredValues + - $inputs + - $inputContext - $unknownInputs[$key] - $value + + + - $inputs + inputs]]> - isValid - isValid + + - RedundantConditionGivenDocblockType - RedundantConditionGivenDocblockType + + - $name - $name + + - $data + - $name + collectionMessages]]> collectionMessages]]> invalidInputs]]> @@ -74,33 +62,33 @@ collectionValues]]> - TFilteredValues + - $data + - $data + collectionValues]]> - array[] - array[] + + - DocblockTypeContradiction - DocblockTypeContradiction - DocblockTypeContradiction - RedundantConditionGivenDocblockType - RedundantConditionGivenDocblockType - RedundantConditionGivenDocblockType + + + + + + - self + @@ -109,38 +97,38 @@ - $key - $name - $priority - $value + + + + - $filter - $inputSpecification + + - $key - $name - $options - $priority - $value + + + + + - new $class() + - $value - $value + + - DeprecatedMethod - DocblockTypeContradiction - DocblockTypeContradiction - RedundantConditionGivenDocblockType + + + + @@ -148,49 +136,49 @@ prepareRequiredValidationFailureMessage()]]> - $rawValue + - $value - $value + + - $fileData - $newValue[] - $rawValue - $value - $value + + + + + - is_array($rawValue) - is_array($rawValue) + + - $rawValue[0] + - $fileData - $newValue[] - $rawValue - $value - $value + + + + + - UploadedFileInterface|UploadedFileInterface[] + filter($value)]]> - $value + - $newValue + - $rawValue + @@ -198,7 +186,7 @@ errorMessage)]]> - null|string + prepareRequiredValidationFailureMessage()]]> @@ -207,94 +195,94 @@ getTranslatorTextDomain()]]> - $translator - $value + + - getOption - getTranslator - getTranslatorTextDomain + + + - (bool) $allowEmpty - (bool) $breakOnFailure - (bool) $continueIfEmpty - (string) $errorMessage + + + + - $input + - $input + - InputFilterAbstractServiceFactory + - $config + - $allConfig - $config + + - $cName - $container - $container - $rName - $rName - $services + + + + + + - getInputFilter - setInputFilter + + - InputFilterInterface + - InputFilterInterface + - null|ConfigInterface|ContainerInterface + - InputFilterPluginManager - InputFilterPluginManager + + - get + ? T1 : ($name is class-string ? T2 : InputInterface|InputFilterInterface))]]> - parent::get($name, $options) + - $factories + - validatePlugin + - $shareByDefault + initializers]]> @@ -306,43 +294,43 @@ - InputFilterPluginManagerFactory + - $container + - setCreationOptions + - MismatchingDocblockParamType - MismatchingDocblockParamType - MoreSpecificImplementedParamType + + + - $this - $this - $this - $this - $this - $this - $this - $this + + + + + + + + - $moduleManager + - ModuleManager + - $valueMap + ]]> - string[] + - $dataSets + ]]> - $set - $set - $set + + + - - input->getValue()]]> - - $set[1] - $set[2] - $set[4] + + + - $set[1] - $set[2] - $set[4] + + + - $value - $values[0] - $values[0] - $values[1] + + + + - $dataSets - $dataSets + + |object * }>]]> + + + - isArray - isArray + + - enableProxyingToOriginalMethods - enableProxyingToOriginalMethods + + ['nested-input1', 'nested-input2']]]]> - $dataSets + - $inputTypeData - $inputTypeData + + $inputTypeData[1]]]> @@ -432,37 +420,37 @@ - $input - $set[5] - $set[6] + + + - $getMessages - $msg - $msg + + + - $inputTypeData[1] - $inputTypeData[2] + + - $set[0][$name] - $set[5][$name] - $set[6][$name] + + + - $expectedRawValue - $expectedValue - $input - $input - $set[0][$name] - $set[5][$name] - $set[6][$name] - $tmpTemplate[2] - $tmpTemplate[3] + + + + + + + + + - $dataSets + , * 1: iterable, @@ -483,34 +471,34 @@ * }>]]> - $input - $input - $input + + + - $expectedInputName - $expectedInputName - $expectedInputName - $expectedInputName - $expectedInputName + + + + + - getName - getName + + - addMethodArgumentsProvider - contextProvider - setDataArgumentsProvider - unknownScenariosProvider + + + + - $inputName + - $errorMessage + ]]> - contextProvider - countVsDataProvider - dataNestingCollection - dataVsValidProvider - inputFilterProvider - invalidCollections - invalidDataType - isRequiredProvider + + + + + + + + - getPluginManager - getPluginManager + + - breakOnFailure - breakOnFailure + + - inputTypeSpecificationProvider + - continueIfEmpty + @@ -573,13 +561,13 @@ - $dataSets + - iterable + - $input + @@ -587,7 +575,7 @@ - UploadedFileInterface + ]]> - $input + - $generator instanceof Generator + - getInstanceOf + - pluginProvider + @@ -649,8 +637,8 @@ * }>]]> - defaultInvokableClassesProvider - serviceProvider + + @@ -662,7 +650,7 @@ * }>]]> - $dataSets + ]]> - $inputFilter + - $factory + @@ -739,20 +727,20 @@ * }>]]> - array_merge($emptyValues, $mixedValues) + - $tmpTemplate[4] - $value + + - method - willReturn - with + + + ]]> - $translator + - expects + - isArray + - getParam + - getEvent + - addServiceManager + - collectionCountProvider + diff --git a/src/ArrayInput.php b/src/ArrayInput.php index bbdb6e7d..0c39f51c 100644 --- a/src/ArrayInput.php +++ b/src/ArrayInput.php @@ -4,60 +4,30 @@ namespace Laminas\InputFilter; -use function gettype; +use Laminas\Validator\IsArray; + +use function array_map; +use function assert; use function is_array; -use function sprintf; class ArrayInput extends Input { - /** @var array */ - protected $value = []; - - /** - * @inheritDoc - * @param array $value - * @throws Exception\InvalidArgumentException - */ - public function setValue($value) + /** @inheritDoc */ + public function getValue() { - if (! is_array($value)) { - throw new Exception\InvalidArgumentException(sprintf( - 'Value must be an array, %s given.', - gettype($value) - )); + if (! is_array($this->value)) { + return $this->value; } - parent::setValue($value); - - return $this; - } - /** - * {@inheritdoc} - */ - public function resetValue() - { - $this->value = []; - $this->hasValue = false; - return $this; - } - - /** - * @return array - */ - public function getValue() - { $filter = $this->getFilterChain(); - $result = []; - foreach ($this->value as $key => $value) { - $result[$key] = $filter->filter($value); - } - return $result; + + return array_map( + static fn (mixed $value): mixed => $filter->filter($value), + $this->value, + ); } - /** - * @param mixed $context Extra "context" to provide the validator - * @return bool - */ + /** @inheritDoc */ public function isValid($context = null) { $hasValue = $this->hasValue(); @@ -76,11 +46,23 @@ public function isValid($context = null) return false; } + if (! $hasValue && ! $required) { + return true; + } + if (! $this->continueIfEmpty() && ! $this->allowEmpty()) { $this->injectNotEmptyValidator(); } + + $values = $this->getValue(); + + if (! is_array($values)) { + $this->errorMessage = $this->prepareNotArrayFailureMessage(); + + return false; + } + $validator = $this->getValidatorChain(); - $values = $this->getValue(); $result = true; if ($required && empty($values)) { @@ -112,4 +94,23 @@ public function isValid($context = null) return $result; } + + /** @return array */ + private function prepareNotArrayFailureMessage(): array + { + $chain = $this->getValidatorChain(); + $isArray = $chain->plugin(IsArray::class); + + foreach ($chain->getValidators() as $validator) { + if ($validator['instance'] instanceof IsArray) { + $isArray = $validator['instance']; + break; + } + } + + $result = $isArray->isValid($this->getValue()); + assert($result === false); + + return $isArray->getMessages(); + } } diff --git a/test/ArrayInputTest.php b/test/ArrayInputTest.php index db2525b7..4bb76c31 100644 --- a/test/ArrayInputTest.php +++ b/test/ArrayInputTest.php @@ -6,10 +6,12 @@ use Laminas\Filter\FilterChain; use Laminas\InputFilter\ArrayInput; -use Laminas\InputFilter\Exception\InvalidArgumentException; +use Laminas\InputFilter\InputFilter; +use Laminas\Validator\IsArray; use Laminas\Validator\NotEmpty as NotEmptyValidator; use Laminas\Validator\ValidatorChain; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; use Webmozart\Assert\Assert; @@ -29,7 +31,7 @@ protected function setUp(): void public function testDefaultGetValue(): void { - self::assertCount(0, $this->input->getValue()); + self::assertNull($this->input->getValue()); } public function testArrayInputMarkedRequiredWithoutAFallbackFailsValidationForEmptyArrays(): void @@ -59,13 +61,6 @@ public function testArrayInputMarkedRequiredWithoutAFallbackUsesProvidedErrorMes self::assertEquals($expected, $message); } - public function testSetValueWithInvalidInputTypeThrowsInvalidArgumentException(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Value must be an array, string given'); - $this->input->setValue('bar'); - } - /** * @psalm-return arrayadd([ + 'type' => ArrayInput::class, + 'validators' => [ + ['name' => NotEmptyValidator::class], + ], + ], 'myInput'); + + $inputFilter->setData(['myInput' => ['foo', 'bar']]); + self::assertTrue($inputFilter->isValid()); + } + + /** @return array */ + public static function nonArrayInput(): array + { + return [ + 'String' => ['foo'], + 'Empty String' => [''], + 'Null' => [null], + 'Object' => [(object) ['foo' => 'bar']], + 'Float' => [1.23], + 'Integer' => [123], + 'Boolean' => [true], + ]; + } + + #[DataProvider('nonArrayInput')] + public function testNonArrayValueIsValidationFailure(mixed $value): void + { + $this->input->setValue($value); + self::assertFalse($this->input->isValid()); + } + + #[DataProvider('nonArrayInput')] + public function testNonArrayInputViaInputFilterIsUnacceptable(mixed $value): void + { + $inputFilter = new InputFilter(); + $inputFilter->add([ + 'type' => ArrayInput::class, + 'validators' => [ + ['name' => NotEmptyValidator::class], + ], + ], 'myInput'); + + $inputFilter->setData(['myInput' => $value]); + self::assertFalse($inputFilter->isValid()); + $messages = $inputFilter->getMessages()['myInput'] ?? null; + self::assertIsArray($messages); + self::assertArrayHasKey(IsArray::NOT_ARRAY, $messages); + self::assertIsString($messages[IsArray::NOT_ARRAY]); + self::assertStringStartsWith( + 'Expected an array value but ', + $messages[IsArray::NOT_ARRAY], + ); + } }