PHPUnit testing a Laminas/Form with custom validator with dependencies

Hey,

I am trying to test a Laminas/Laminas-Form, that has a custom validator and this validator has a dependency that gets not injected.
If I run the application in a normal environment it is working as expected. Only the test environment is affected.

As far as I can see, if I run $myForm->isValid() at some point of the ValidationChain a new PluginManager is created if not present. But this manager does not know the application configuration and assumes that my MyCustomValidatorWithDependencies can be invoked by using the InvokableFactory, which is obviously not the case. Is there a way to inject the correct application configuration into the PluginManager or just a single factory?
I also checked that, in a normal environment the PluginManager is present and aware of the correct factory of my MyCustomValidatorWithDependencies before and during $myForm->isValid() is executed.

<?php

// AppTest\Form\MyFormTest
class MyFormTest extends TestCase
{
    public function testIsValid(): void
    {
        $myForm = new MyForm();
        $myForm->setData($data);

        $makeAssertionForIsValid = $myForm->isValid();
        $makeAssertionForMessages = $myForm->getMessages();
    }
}

// App\Form\MyForm
class MyForm extends Form implements InputFilterProviderInterface {

    public function __construct() {
        parent::__construct('myFormName');
        $this->setInputFilter(new InputFilter());
    }

    public function getInputFilterSpecification(): array
    {
        return [
            'myValue' => [
                'validators' => [
                    [
                        'name' => MyCustomValidatorWithDependencies::class,
                    ],
                ],
            ],
        ];
    }
}

// App\Validator\MyCustomValidatorWithDependencies
class MyCustomValidatorWithDependencies extends AbstractValidator
{
    public function __construct(
        MyCustomDependency $myCustomDependency,
        $options = []
    ) {
        $this->myCustomDependency = $myCustomDependency;
        parent::__construct($options);
    }


    public function isValid($value) {
        // do validation...
    }
}

// App\Validator\Factory\MyCustomValidatorWithDependenciesFactory
class MyCustomValidatorWithDependenciesFactory implements FactoryInterface {
    public function __invoke(
        ContainerInterface $container,
        $requestedName,
        array $options = null
    ) {
        return new MyCustomValidatorWithDependencies(
            $container->get(MyCustomDependency::class),
            $options,
        );
    }
}


// App\config\module.config.php
return [
    'service_manager' => [
        'factories' => [
            App\Validator\MyCustomValidatorWithDependencies::class => App\Validator\Factory\MyCustomValidatorWithDependenciesFactory::class,
            App\Dependency\MyCustomDependency::class => App\Dependency\Factory\MyCustomDependencyFactory::class,
        ],
    ],
    'validators' => [
        'factories' => [
            App\Validator\MyCustomValidatorWithDependencies::class => App\Validator\Factory\MyCustomValidatorWithDependenciesFactory::class,
        ],
    ],
];

It is correct that a form created via new Form() does not know anything from a (configured) plugin manager from outside. No magic is used here.

In the documentation of laminas-form you will find the following description:

And this important paragraph:

Instantiating the Form

The form element manager is used instead of directly instantiating the form to ensure to get the input filter manager injected. This allows usage of any input filter registered with the input filter managers which includes custom filters and validators.

This means that different plugin managers are used to create a form:

  • the form element manager uses the input filter plugin manager
  • the input filter plugin manager uses the validator manager and the filter manager

For your test you need to do the following:

  • setup the validator manager
  • setup the input filter plugin manager
  • setup the form element manager

Then the your custom validator can be used within your form / input filter definition.

I hope this helps to run your tests.

Hey,

you are right, I just missed one line. And the more stupid thing is, I have this line in the factory of the controller where I use MyForm. :roll_eyes:

I changed my test case to the following and it is working as I expect.

class MyFormTest extends TestCase
{
    public function testIsValid(): void
    {
        // create a mock
        $myCustomDependency = $this->createStub(MyCustomDependency::class);
        
        // overwrite the factory in the application service locator to return
        // the mock instead of the real dependency.
        $container->setFactory(
            MyCustomDependency::class,
            static function () use
            (
                $myCustomDependency
            ) {
                return $myCustomDependency;
            }
        );
        
        // this is the important line
        // get the form
        $formManager = $container->get('FormElementManager');
        $myForm = $formManager->get(MyForm::class);
        $myForm->setData($data);

        $makeAssertionForIsValid = $myForm->isValid();
        $makeAssertionForMessages = $myForm->getMessages();
    }
}

Thank you for your help.