简体   繁体   中英

How to inject a dependency into a Validator in Zend Framework 3?

I have created a custom Validator, called CredentialsValidator, that returns true, if passed credentials are valid. The actual validation of credentials is the responsibility of the dependency AccountService, which is available in the Validator via the getAccountService() method. The interesting part of the CredentialsValidator::isValid($value, $context = null) is:

$accountService = $this->getAccountService();

$accountService->setEmail($this->getEmail());
$accountService->setPassword($this->getPassword());

try {
    $accountService->auth();
} catch (RuntimeException $exception) {
    $this->setMessage($exception->getMessage());
    $this->error(self::INVALID_CREDENTIALS);

    return false;
}

The properties $email and $password are populated like this:

if (isset($context['email'])) {
    $this->setEmail($context['email']);
}

if (isset($context['password'])) {
    $this->setPassword($context['password']);
}

When I instantiate the CredentialsValidator in my unit tests and manually assign the dependency AccountService, it works 100% correctly.

In the actual web application, AccountService is instantiated via the ServiceManager, using standard configuration in module.config.php:

return [
    'service_manager' => [
        'factories' => [
            AccountServiceFactory::class   => AccountServiceFactory::class,
        ],
    ]
];

My goal, however, is to create a typical "sign in" form, that uses the CredentialsValidator to validate the users credentials.

To do this, I have created a form, extending Zend\\Form\\Form:

$this->add([
    'type'  => 'text',
    'name' => 'email',
    'attributes' => [
        'id' => 'email'
    ],
    'options' => [
        'label' => 'Email',
    ],
]);

$this->add([
    'type'  => 'password',
    'name' => 'password',
    'attributes' => [
        'id' => 'password'
    ],
    'options' => [
        'label' => 'Password',
    ],
]);

And the associated Model, defining the getInputFilter() method:

public function getInputFilter()
{
    if ($this->inputFilter) {
        return $this->inputFilter;
    }

    $this->inputFilter = new InputFilter();

    $this->inputFilter->add([
        'name'     => 'email',
        'required' => true,
        'filters'  => [
            ['name' => StringTrimFilter::class],
            ['name' => StripTagsFilter::class],
            ['name' => StripNewlinesFilter::class],
        ],
        'validators' => [
            [
                'name' => EmailAddressValidator::class,
                'break_chain_on_failure' => true,
                'options' => [
                    'allow' => HostnameValidator::ALLOW_DNS,
                    'useMxCheck' => false,
                ],
            ],

        ],
    ]);

    $this->inputFilter->add([
        'name'     => 'password',
        'required' => true,
        'filters'  => [
            ['name' => StringTrimFilter::class],
            ['name' => StripTagsFilter::class],
            ['name' => StripNewlinesFilter::class],
        ],
        'validators' => [
            [
                'name'    => StringLengthValidator::class,
                'break_chain_on_failure' => true,
                'options' => [
                    'min' => 1,
                    'max' => 128
                ],
            ]
        ],

    ]);

And this is where the problem starts. When I add:

[
    'name' => CredentialsValidator::class,
    'break_chain_on_failure' => true,
],

to the 'validators' key of the 'password' field, I cannot inject the required dependency, which is stored in the ServiceManager, and consequently, the CredentialsValidator cannot work, as it does not have access to the AccountService instance.

I have come up with 2 solutions to this problem, one of which I immediately discarded, as it uses a singleton, and the other, while it works, requires passing the dependency manually.

Solution #1: Using a singleton that is created in Module.php

In the onBootstrap(MvcEvent $event) method, it is possible to create a singleton:

AccountService::getInstance()

which can then be accessed in the CredentialsValidator, and calling Controller.

I discarded this solution, as it uses the now deprecated Singleton pattern.

Solution #2: Manually passing the AccountService instance

In the Controller, it is possible to pass the AccountService instance into the Model's constructor:

$model = new Model([AccountService::class => $accountService]);

and then in Model::getInputFilter(), pass the instance to the 'validators' key of the 'password' field, like this:

$this->inputFilter->add([
    'name'     => 'password,
    'required' => true,
    'filters'  => [
        ['name' => StringTrimFilter::class],
        ['name' => StripTagsFilter::class],
        ['name' => StripNewlinesFilter::class],
    ],
    'validators' => [
        [
            'name'    => StringLengthValidator::class,
            'break_chain_on_failure' => true,
            'options' => [
                'min' => 1,
                'max' => 128
            ],
        ],
        [
            'name' => CredentialsValidator::class,
            'break_chain_on_failure' => true,
            'options' => [
                AccountService::class => $this->getAccountService(),
            ],
        ],
    ],

]);

The CredentialsValidator then simply needs to accept the dependency via its constructor:

if (array_key_exists(AccountService::class, $options)) {
    $this->setAccountService($options[AccountService::class]);
}

This solution does work, and it does respect the interfaces between the classes, however, it is considerable additional work to manually pass the AccountService instance around, and indeed, the whole point of the ServiceManager and injection is to avoid this. Solution #2 feels like a foreign body in a Zend Framework 3 application.

My question: How can I access the AccountService instance in the CredentialsValidator without manually passing it from the Controller?

Thank you kindly in advance.

I think you can create a Factory for CredentialsValidator . Then register the factory inside validators configuration in module.config.php or inside getValidatorConfig() inside Module.php .

Example: module.config.php

'service_manager' => [
    'factories' => [
    ]
],
'validators' => [
    'factories' => [
          CredentialsValidator::class => CredentialsValidatorFactory::class
     ]
]

or Module.php

public function getValidatorConfig()
{
    return [
        'factories' => [
             CredentialsValidator::class => new                                         CredentialsValidatorFactory::class($param, $param2)
        ]
    ]
}

Because of the validator has been registered, you can just registered name in InputFilter configuration

$this->inputFilter->add([
     'name'     => 'Credential',
    'required' => true,        
    'validators' => [
         [
            'name' => CredentialsValidator::class,
         ]
    ],
]);

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM