About zf3 validator of RecordExists

In my global.php file:

return [
    'db' => [
        'driver' => 'Pdo',
        'dsn'    => 'mysql:dbname=admin;host=localhost;charset=utf8',
    ],
    'main' => [ // this is the database I wanna to use for some RecordExist validator!!!
        'driver' => 'Pdo',
        'dsn'    => 'mysql:dbname=main;host=localhost;charset=utf8',
    ],
    'msdb' => [
        'platform' => 'SqlServer',
        'dirver' => 'Pdo',
        'dsn'    => 'dblib:host=sds2something.com;dbname=somedbname_db;',
        'charset' => 'UTF-8',
        'pdotype' => 'dblib',
    ],
]

I used to config the validator as an array in the entity model as follow:

class SomeEntity implements InputFilterAwareInterface
{
    //some columns here as attributes
    ...
    private $inputFilter;

    public function exchangeArray(array $data){...}

    public function getArrayCopy(){...}

    public function setInputFilter(InputFilterInterface $inputFilter){...}

    public function getInputFilter()
    {
        ...
        $inputFilter->add([
            'name' => 'customerid',
            'required' => true,
            'filters' => [
                ['name' => ToInt::class],
            ],
            'validators' => [
                [
                    'name' => RecordExists::class,
                    'options' => [
                        'table' => 'main_customers',
                        'field' => 'id',
                        'adapter' => $dbAdapter, //TODO: how to pass this $var !!!!!???????
                        'message' => 'this customer id is not exist',
                    ],
                ],
            ],
        ]);
        ...
    }
    
}

In the controller, it is normal to new the entity like following:

class SomeController extends AbstractActionController
{

    ...

    public function addsomethingAction()
    {
        ...

        $role = new Role(); //should I pass the $dbadapter here? or there is a more stander way?
        $form->setValidationGroup('customerid','name', 'description');
        $form->setInputFilter($role->getInputFilter());
        $form->setData($request->getPost());

        if (!$form->isValid()) return ['form' => $form];

        $role->exchangeArray($form->getData());
        $this->roleTable->saveRole($role, $this->adminTable);

        ...

    }

    
}

Some other way?

If it is the best way to pass the $dbadapter via the __construction() function, it means that I need to write a __construction in the entityModel, it is abnormal to do so I think.

I’d avoid using the InputFilterAwareInterface here: it was done as a way to quickly convey data+specification, but here you’d really split it, and make your form a separate service instead.

@ocramius

However it seems it is the standard way that the official tutorial shows.

Then let me disagree with the official tutorial :stuck_out_tongue:

It is much easier to create a factory for the DB validators, because with this solution, you do not need to pass the database adapter to any form or input-filter which uses the DB validators.

@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!