About zf3 validator of RecordExists

@froschdesign
Hi Frank. thanks for your reply! Can you give me some demo?

I think your problem is not creating a factory, just registering that factory:

Example: (e.g. config/autoload/global.php)

// …
'validators'         => [
    'factories' => [
        Zend\Validator\Db\NoRecordExists::class => DbValidatorAbstractFactory::class,
        Zend\Validator\Db\RecordExists::class   => DbValidatorAbstractFactory::class,
    ],
],
// …

@froschdesign
where it can show which db config it used when i configurated several databases?

sometimes we config db1, db2…etc

This is a new requirement that nobody knows except you. Why is this important information not included in your first post / question?
My suggestion won’t work here!


Extract the input-filter and create a factory for it which sets the required database adapter.

@froschdesign

sorry for not mentioned the several db configurations at global.php first. infact, the database i need to use for the recordexist validator is not the default one.

can you give a little more demo codes? i have no idea on the extract of the inputfilter you said above

Nothing special:

CustomerInputFilter extend Zend\InputFilter\InputFilter {}

But you can also register a new element and create a factory:

'validators'         => [
    'factories' => [
        My\Validator\CustomerRecordExists::class => My\Validator\CustomerRecordExistsFactory::class,
    ],
],

The class CustomerRecordExists must not exists and in the factory you use the standard RecordExists class and set the database adapter. Now you can use it in you input-filter configuration.
(This suggestion is based an the fact that your customers always in the same database.)

@froschdesign

it is not possible that. all the valid logic use the same database. any other suggestions?

I think I need to write several custom input validates. it is not bad.

thank you so much for your time!

@froschdesign

Hi Frank,

This “DbValidatorAbstractFactory” you said is a custom validator I write seperately?

It is called entity I think, I remember I saw this defination somewhere. It implements a inputfilter, I am a little confused by your following method.

I need to create a CustomerInputFilter instead the SomeEntity?
Why I need to create a CustomerInputFilter? I always thought I need to create a CustomerInputValidator…

Forget about the several databases configuration in global.php, just make the RecordExist validator for only one databse. If so, I need to create a CustomerRecordExistValidator class to extend AbstractValidator ?

  1. Make sure you have there components. And add modules to modules.config.php

    • zendframework/zend-db
    • zendframework/zend-inputfilter
    • zendframework/zend-filter
    • zendframework/zend-validator
  2. Create a DbValidatorAbstractFactory class. You can direct copy from zfcampus/zf-content-validation > RecordExistsFactory.

  3. Add validator factory config Zend\Validator\Db\RecordExists::class => DbValidatorAbstractFactory::class in your module’s module.config.php.
    It will overrides the default configuration.

  4. If your inputfilter is uncomplicated. Recommend you create inputfilter by InputFilterAbstractServiceFactory. You just config it.

  5. Create a controller factory and inject your inputfilter.

There’s example:

File SomeModule/config/module.config.php code:

use  Zend\Validator\Db\RecordExists;

return [
    //  "db" services factory config.
    'db' => [
        'adapters' => [
             'db.foo' => [
                'driver' => 'Pdo',
                'dsn'    => 'mysql:dbname=admin;host=localhost;charset=utf8',
            ],
            'db.bar' => [
                'driver' => 'Pdo',
                'dsn'    => 'mysql:dbname=admin;host=localhost;charset=utf8',
            ],
       ]
    ],

    // `RecordExists:class` validator factory config.
    'validators'         => [
        'factories' => [
            //It will overrides the default configuration.
            // Default is `RecordExists::class => InvokableFactory::class`
            RecordExists::class => DbValidatorAbstractFactory::class,
        ],
    ],

    // `zend-inputfilter`'s `InputFilterAbstractServiceFactory` config style.
    'input_filter_specs' => [
        'MyInputFilter' => [
            [
                'name'       => 'username',
                'validators' => [
                    [
                         'name' => RecordExists::class,
                         'options' => [
                              'adapter' => 'db.foo',
                              '...' => '...', 
                         ]
                    ]
                ]
            ]
        ]
    ],

    'controllers' => [
         MyController::class => MyControllerFactory::class,
   ]
];

File SomeModule/src/Validator/DbValidatorAbstractFactory.php code:

apigility’s RecordExistsFactory

File SomeModule/src/Controller/MyController.php code:

class MyControllerFactory {
    public function __invoke(ContainerInterface $container) {
        return new MyController($container->get(InputFilterPluginManager::class)->get('MyInputFilter'));
    }
}
class MyController extends AbstractActionController{
    public function __construct(InputFilterInterface $inputFilter) {
         $this->inputFilter = $inputFilter;
    }
    public function someAction() {
          // ...
          $form->setInputFilter($this->inputFilter);
    }
}

If all the validation logic uses the same database, then all my suggestions are possible! :smiley:

No, not validator! The validator is already there: Zend\Validator\Db\RecordExists.
You need only a factory which creates the validator and adds the database adapter.

@froschdesign

I guess I use a different way as you said, as well as @Moln.
You two may have a separate file which store the inputfiler, to me, I put the entity part and inputfilter part together in one file which like

namespace Album\Model;

// Add the following import statements:
use DomainException;
use Zend\Filter\StringTrim;
use Zend\Filter\StripTags;
use Zend\Filter\ToInt;
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilterInterface;
use Zend\Validator\StringLength;

class Album implements InputFilterAwareInterface
{
    public $id;
    public $artist;
    public $title;

    // Add this property:
    private $inputFilter;

    public function exchangeArray(array $data)
    {
        $this->id     = !empty($data['id']) ? $data['id'] : null;
        $this->artist = !empty($data['artist']) ? $data['artist'] : null;
        $this->title  = !empty($data['title']) ? $data['title'] : null;
    }

    /* Add the following methods: */

    public function setInputFilter(InputFilterInterface $inputFilter)
    {
        throw new DomainException(sprintf(
            '%s does not allow injection of an alternate input filter',
            __CLASS__
        ));
    }

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

        $inputFilter = new InputFilter();

        $inputFilter->add([
            'name' => 'id',
            'required' => true,
            'filters' => [
                ['name' => ToInt::class],
            ],
        ]);

        $inputFilter->add([
            'name' => 'artist',
            'required' => true,
            'filters' => [
                ['name' => StripTags::class],
                ['name' => StringTrim::class],
            ],
            'validators' => [
                [
                    'name' => StringLength::class,
                    'options' => [
                        'encoding' => 'UTF-8',
                        'min' => 1,
                        'max' => 100,
                    ],
                ],
            ],
        ]);

        $inputFilter->add([
            'name' => 'title',
            'required' => true,
            'filters' => [
                ['name' => StripTags::class],
                ['name' => StringTrim::class],
            ],
            'validators' => [
                [
                    'name' => StringLength::class,
                    'options' => [
                        'encoding' => 'UTF-8',
                        'min' => 1,
                        'max' => 100,
                    ],
                ],
            ],
        ]);

        $this->inputFilter = $inputFilter;
        return $this->inputFilter;
    }
}

the official link is Forms and Actions - Tutorials - Zend Framework Docs

I guess you use a different code structure comparing to this way. I thought via your way for several days, and can not get a answer that why I need to crate a RecordExistFactory and create another InputFilterFactory!! Because my inputfilter is not a separate file, it is inside the entity-model file.

You do not need an extra InputFilterFactory, I never said that. You must create only a factory for the validator and register this factory:

// …
'validators'         => [
    'factories' => [
        Zend\Validator\Db\NoRecordExists::class => DbValidatorAbstractFactory::class,
        Zend\Validator\Db\RecordExists::class => DbValidatorAbstractFactory::class,
    ],
],
// …

With this solution, it does not matter in which way the input-filter is created.

@froschdesign
Hi Frank, thanks!

Now I took the following steps(I have problem on step 4, step 4 solved, now the problem is that, can not overwrite the default validator RecordExists::class and NoRecordExists::class):

step 1. create a factory for the validator named DbValidatorAbstractFactory, it is beyond the module/Application/src/Validator/Factory folder.

 namespace Application\Validator\Factory;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\Stdlib\ArrayUtils;
use Zend\Validator\Db\RecordExists;

class DbValidatorAbstractFactory implements FactoryInterface
{

    private $options;

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        //exit('test whether it run this function'); //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! nothing happened!!!
        if (isset($options['adapter'])) {
            return new $requestedName(ArrayUtils::merge(
                $options,
                ['adapter' => $container->get($options['adapter'])]
            ));
        }
        return new $requestedName($options);
    }

    public function setCreationOptions(array $options)
    {
        $this->options = $options;
    }
}

step 2 config the new validator factory in the module.config.php like:

'validators' => [
    'factories' => [
        RecordExists::class => DbValidatorAbstractFactory::class,
        NoRecordExists::class => DbValidatorAbstractFactory::class,
    ],
],

step 3 according the vendor\zendframework\zend-db\src\Adapter\AdapterServiceFactory.php, I made a customer dbadatper named ExpressAdapterServiceFactory in the folder module/Application/src/Db/Factory

namespace Application\Db\Factory;

use Interop\Container\ContainerInterface;
use Zend\ServiceManager\Factory\FactoryInterface;

class ExpressAdapterServiceFactory implements FactoryInterface
{

    public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
    {
        $config = $container->get('config');
        return new Adapter($config['express']);
    }
}

step 4, My problem is step 4, I wanna to config this new customer db adatper in the top level global.php, how to config it? Can you teach me?

'service_manager' => [
    'factories' => [
        'adapter.express' => Application\Db\Factory\ExpressAdapterServiceFactory::class, //I dont think it is correct!
    ],
],

step 5, my Logistics.php model file is a combination of entity and inputfilter the same way as the official tutorial way as following:

namespace Application\Model\Main;

use Zend\Db\Adapter\AdapterInterface;
use Zend\Filter\StringToUpper;
use Zend\Filter\StringTrim;
use Zend\Filter\StripTags;
use Zend\Filter\ToFloat;
use Zend\Filter\ToInt;
use Zend\InputFilter\InputFilter;
use Zend\InputFilter\InputFilterAwareInterface;
use Zend\InputFilter\InputFilterInterface;
use Zend\Validator\Between;
use Zend\Validator\Db\NoRecordExists;
use Zend\Validator\Db\RecordExists;
use Zend\Validator\InArray;
use Zend\Validator\NotEmpty;
use Zend\Validator\Regex;
use Zend\Validator\StringLength;


class Logistics implements InputFilterAwareInterface
{
    public $id;
    public $customer; //customer need to check whether the input coustomerid is exist
    ...

    private $inputFilter;

    public function exchangeArray(array $data)
    {
        $this->id = !empty($data['id']) ? $data['id'] : null;
        $this->customer = !empty($data['customer']) ? $data['customer'] : null;
        ...
    }

    public function getArrayCopy()
    {
        return [
            'id' => $this->id,
            'customer' => $this->customer,
            ...
        ];
    }

    public function setInputFilter(InputFilterInterface $inputFilter)
    {
        throw new \DomainException(sprintf(
            '%s does not allow injection of an alternate input filter',
            __CLASS__
        ));
    }


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

        $inputFilter = new InputFilter();

        $inputFilter->add([
            'name' => 'id',
            'required' => true,
            'filters' => [
                ['name' => ToInt::class],
            ],
        ]);

        $inputFilter->add([
            'name' => 'customer',
            'required' => true,
            'filters' => [
                ['name' => ToInt::class],
            ],
            'validators' => [
                [
                    'name' => RecordExists::class,
                    'options' => [
                        'table' => 'main_customers',
                        'field' => 'id',
                        'message' => 'customer id input is not exist in the customer table',
                        'adapter' => 'adapter.express',
                    ],
                ],
            ],
        ]);

        ....

        $this->inputFilter = $inputFilter;
        return $this->inputFilter;
    }
}

Step 4: config

'service_manager' => [
    'factories' => [
        // The key name is custom service names, but recommend using fully-qualified class names.
        // Read more infomation to see:
        // https://docs.zendframework.com/zend-expressive/v3/features/container/intro/#service-names
        //
        // The value must be a factory class.
        'db.express' => Application\Db\Factory\ExpressAdapterServiceFactory::class,
    ],
],

Get in ServiceManager(ContainerInterface).

class SomeFactory {
  public function __invoke(ContainerInterface $container) {
    $container->get('db.express');
  }
}

You need to learn more about zend-servicemanager factory.

Then, you can configure it in the validator.

[
  'name' => RecordExists::class,
  'options' => [
    'adapter' => 'db.express'
  ]
]

@Moln

It is ok that use the ‘db.express’ as the factory key, because I can not find a better one, thank you so much!

@froschdesign @Moln

Pls look at the above 5 steps I listed(I edited my reply and added the step 5), it is almost the whole logic for this part, pls help me out!

In fact, I took the above 5 steps and submit the form then I got a error when I use

    $inputFilter->add([
        'name' => 'customer',
        'required' => true,
        'filters' => [
            ['name' => ToInt::class],
        ],
        'validators' => [
            [
                'name' => RecordExists::class,
                'options' => [
                    'table' => 'main_customers',
                    'field' => 'id',
                    'message' => 'custome is not in the database',
                    'adapter' => 'db.express',
                ],
            ],
        ],
    ]);

Argument 1 passed to Zend\Validator\Db\AbstractDb::setAdapter() must be an instance of Zend\Db\Adapter\Adapter, string given, called in /opt/www/websites/somedomain.com/vendor/zendframework/zend-validator/src/AbstractValidator.php on line 139

It says that, it need a adatper instance, But I passed a string.

I guess the replacement of the default recordexist validator not happen, I can not overwrite the default RecordExists::class and NoRecordExists::class eighter I put the configuration in global.php or module.config.php, why?

I moved the following configs from module.config.php to global.php

'validators' => [
    'factories' => [
        Zend\Validator\Db\NoRecordExists::class => Application\Validator\Factory\DbValidatorAbstractFactory::class,
        Zend\Validator\Db\RecordExists::class => Application\Validator\Factory\DbValidatorAbstractFactory::class,
    ],
],

I’m sorry, but it is hard to follow you here and what you may have forgotten or overlooked. Can you create a repository and provide the link?

@froschdesign

Now forget about the factory stuff, now my question concentrate on that I can not overwrite the built-in validator RecordExists when I use the following configuration in the module.config.php.

'validators' => [
    'factories' => [
        Zend\Validator\Db\NoRecordExists::class => Application\Validator\Factory\DbValidatorAbstractFactory::class,
        Zend\Validator\Db\RecordExists::class => Application\Validator\Factory\DbValidatorAbstractFactory::class,
    ],
],

The above configuration not work, because when I use RecordExists::class in the inputfilter, it calls the built-in validator, not the factory I registered with the above configuration.
I don’t know why.

Is zend-validator registered as module in application.config.php?

I have provided an example in this.

I think the class DbValidatorAbstractFactory merge to zend-validator will be better.