Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Array input refactor #34

Merged
merged 6 commits into from
Feb 28, 2024
Merged
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
8 changes: 4 additions & 4 deletions psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@
<code><![CDATA[validationMatchProvider]]></code>
</PossiblyUnusedMethod>
</file>
<file src="test/InputFilter/InputFilterVisitorTest.php">
<file src="test/InputFilter/AbstractInputVisitorTest.php">
<PossiblyUnusedMethod>
<code><![CDATA[collectionProvider]]></code>
<code><![CDATA[addNotEmptyProvider]]></code>
</PossiblyUnusedMethod>
</file>
<file src="test/InputFilter/InputVisitorTest.php">
<file src="test/InputFilter/InputFilterVisitorTest.php">
<PossiblyUnusedMethod>
<code><![CDATA[addNotEmptyProvider]]></code>
<code><![CDATA[collectionProvider]]></code>
</PossiblyUnusedMethod>
</file>
<file src="test/Psalm/IsFqcnTypeTraitTest.php">
Expand Down
38 changes: 17 additions & 21 deletions src/ConfigProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -158,7 +156,6 @@ private function getLaminasFormShapeConfig(): array
],
'input-visitors' => [
ArrayInputVisitor::class,
CollectionInputVisitor::class,
InputVisitor::class,
],
'filter' => [
Expand Down Expand Up @@ -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,
],
];
}
Expand Down
7 changes: 6 additions & 1 deletion src/Form/FormVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
116 changes: 116 additions & 0 deletions src/InputFilter/AbstractInputVisitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

declare(strict_types=1);

namespace Kynx\Laminas\FormShape\InputFilter;

use Kynx\Laminas\FormShape\FilterVisitorInterface;
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\Union;

use function array_filter;
use function array_map;
use function array_unshift;

abstract readonly class AbstractInputVisitor implements InputVisitorInterface
{
/**
* @param array<FilterVisitorInterface> $filterVisitors
* @param array<ValidatorVisitorInterface> $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<ValidatorInterface> $validators
* @return array<ValidatorInterface>
*/
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;
}
}
68 changes: 68 additions & 0 deletions src/InputFilter/AbstractInputVisitorFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Kynx\Laminas\FormShape\InputFilter;

use Kynx\Laminas\FormShape\ConfigProvider;
use Kynx\Laminas\FormShape\FilterVisitorInterface;
use Kynx\Laminas\FormShape\ValidatorVisitorInterface;
use Psr\Container\ContainerInterface;

/**
* @psalm-import-type FormShapeConfigurationArray from ConfigProvider
*/
abstract readonly class AbstractInputVisitorFactory
{
abstract public function __invoke(ContainerInterface $container): AbstractInputVisitor;

/**
* @return array<FilterVisitorInterface>
*/
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<ValidatorVisitorInterface>
*/
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<T> $visitorName
* @return T
*/
private function getVisitor(
ContainerInterface $container,
string $visitorName
): FilterVisitorInterface|ValidatorVisitorInterface {
if ($container->has($visitorName)) {
return $container->get($visitorName);
}

return new $visitorName();
}
}
52 changes: 39 additions & 13 deletions src/InputFilter/ArrayInputVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
7 changes: 5 additions & 2 deletions src/InputFilter/ArrayInputVisitorFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
}
Loading