phpvalidationzend-framework3zend-validatezend-servicemanager

Zendframework 3 - Overwrite CSRF Validator


I try to migrate from ZF2 to ZF3 but many viewHelpers and validators do not work. But only the ones who overwrite ZendFrameworks viewhelpers / validators do not work...

I want f.e. to overwrite the CSRF validator to allow a higher timeout by default.

I have the following application configuration:

$config = array(
// This should be an array of module namespaces used in the application.
'modules' => array(
    'Zend\Cache',
    'Zend\Db',
    'Zend\Log',
    'Zend\Mail',
    'Zend\Mvc\Console',
    'Zend\Mvc\I18n',
    'Zend\I18n',
    'Zend\Mvc\Plugin\FilePrg',
    'Zend\Form',
    'Zend\Hydrator',
    'Zend\InputFilter',
    'zend\Form',
    'Zend\Filter',
    'Zend\Mvc\Plugin\FlashMessenger',
    'Zend\Mvc\Plugin\Identity',
    'Zend\Mvc\Plugin\Prg',
    'Zend\Navigation',
    'Zend\Paginator',
    'Zend\Serializer',
    'Zend\ServiceManager\Di',
    'Zend\Session',
    'Zend\Router',
    'Zend\Validator',
    'DoctrineModule',
    'DoctrineORMModule',
    'TwbBundle',
    'AssetManager',
    #'Reliv\ElFinder',
    'ZfcUser', //https://github.com/ZF-Commons/ZfcUser
    'ZfcUserDoctrineORM',
    'BjyAuthorize', // https://github.com/bjyoungblood/BjyAuthorize
    'Base',
    'Product',
    'Blog',
    'Admin'
),

// These are various options for the listeners attached to the ModuleManager
'module_listener_options' => array(
    // This should be an array of paths in which modules reside.
    // If a string key is provided, the listener will consider that a module
    // namespace, the value of that key the specific path to that module's
    // Module class.
    'module_paths' => array(
        './module',
        './vendor',
    ),

    // An array of paths from which to glob configuration files after
    // modules are loaded. These effectively override configuration
    // provided by modules themselves. Paths may use GLOB_BRACE notation.
    'config_glob_paths' => array(
        'config/autoload/{,*.}{global,local}.php',
    ),

    // Whether or not to enable a configuration cache.
    // If enabled, the merged configuration will be cached and used in
    // subsequent requests.
    // 'config_cache_enabled' => true,

    // The key used to create the configuration cache file name.
    //'config_cache_key' => $stringKey,

    // Whether or not to enable a module class map cache.
    // If enabled, creates a module class map cache which will be used
    // by in future requests, to reduce the autoloading process.
    // 'module_map_cache_enabled' => true,

    // The key used to create the class map cache file name.
    #'module_map_cache_key' => $stringKey,

    // The path in which to cache merged configuration.
    'cache_dir' => "data/cache/",

    // Whether or not to enable modules dependency checking.
    // Enabled by default, prevents usage of modules that depend on other modules
    // that weren't loaded.
    // 'check_dependencies' => true,
),

// Used to create an own service manager. May contain one or more child arrays.
//'service_listener_options' => array(
//     array(
//         'service_manager' => $stringServiceManagerName,
//         'config_key'      => $stringConfigKey,
//         'interface'       => $stringOptionalInterface,
//         'method'          => $stringRequiredMethodName,
//     ),
// )

// Initial configuration with which to seed the ServiceManager.
// Should be compatible with Zend\ServiceManager\Config.
// 'service_manager' => array(),
);

Module config of Base module:

namespace Base;
...
return [
    ...
    'validators' => array(
        'invokables' => [
            \Zend\Validator\Csrf::class => Validator\Csrf::class
        ]
    )
    ...
]

Base\Validator\Csrf:

<?php

namespace Base\Validator;


class Csrf extends \Zend\Validator\Csrf
{
    protected $timeout = 1;

    public function __construct($options = [])
    {
        parent::__construct($options);

        die("THIS DOES NOT GETTING PRINTED! NOR DOES THE BREAKPOINT HIT.");
    }
}

EDIT: Added autoload config

composer.json:

"autoload": {
    "psr-4": {
      "Base\\": "module/Base/src/"
    }
  }

EDIT 2: Possible bug in implementation of \Zend\Form\Element\Csrf ?

Interesting, the CsrfValidator just gets directly instantiated here...

/**
 * Get CSRF validator
 *
 * @return CsrfValidator
 */
public function getCsrfValidator()
{
    if (null === $this->csrfValidator) {
        $csrfOptions = $this->getCsrfValidatorOptions();
        $csrfOptions = array_merge($csrfOptions, ['name' => $this->getName()]);
        $this->setCsrfValidator(new CsrfValidator($csrfOptions));
    }
    return $this->csrfValidator;
} 

Stacktrace (breakpoint in \Zend\Validator\Csrf __construct()) Stacktrace

StaticPage is another module of mine.

I also debugged with xdebug and set a break point in the CsrfFactory (return statement) to see, if it is used (but it isn't). I thought I can overwrite services / validators etc. easily in ZF3... Did I miss something?


Solution

  • You can use a delegator to change the validator attached to the form element when the element is instantiated. Essentially a delegator allows you (in this case) to modify the form element after it has been constructed - the idea is explained well here.

    In your case you would create a class:

    <?php
    
    namespace Base\Delegator;
    
    use Zend\ServiceManager\ServiceLocatorInterface;
    use Zend\ServiceManager\DelegatorFactoryInterface;
    use Zend\Validator\Csrf as CsrfValidator;
    
    class CsrfDelegatorFactory implements DelegatorFactoryInterface
    {
        public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback)
        {
            // construct the Csrf form element
            $element = call_user_func($callback);
    
            // set the validator with chosen timeout and other options
            $element->setCsrfValidator(new CsrfValidator(
                [
                    // ...
                    'timeout' => 10000
                ]
            ));
    
            return $element;
        }
    }
    

    Then map the delegator to Zend\Form\Element\Csrf in your application module.config.php:

    'form_elements' => [
        // ...
        'delegators' => [
            \Zend\Form\Element\Csrf::class => [
                0 => \Base\Delegator\CsrfDelegatorFactory::class
            ],
        ]
    ],
    

    Note that this is only changing the validator assigned by default to a Zend\Form\Element\Csrf and a csrf validator obtained by other means will not be affected.