laminas-api-tools / api-tools

Laminas API Tools module for Laminas
https://api-tools.getlaminas.org/documentation
BSD 3-Clause "New" or "Revised" License
37 stars 19 forks source link

ZF\ContentValidation\Validator\Db\[No]RecordExists exclude current entity by default? #12

Open weierophinney opened 4 years ago

weierophinney commented 4 years ago

I’m in running into a rather tricky situation, using NoRecordExists validtor within Apigility.

It’s quite easy to setup and force a field value to be unique like this:

'validators' => array(
    0 => array(
        'name' => 'ZF\\ContentValidation\\Validator\\Db\\NoRecordExists',
        'options' => array(
            'adapter' => 'Zend\\Db\\Adapter\\Adapter',
            'table' => 'mytable',
            'field' => 'myuniquefield',
        ),
    ),
)

However, this does not really work in real-life, since only POST requests deliver the expected results. Once you want to modify a records using PUT, this fails, because … well … the record exists already. In case of PUT/PATCH, I need to set up the validator to exclude the entity that’s being updated, like this:

'validators' => array(
    0 => array(
        'name' => 'ZF\\ContentValidation\\Validator\\Db\\NoRecordExists',
        'options' => array(
            'adapter' => 'Zend\\Db\\Adapter\\Adapter',
            'table' => 'mytable',
            'field' => 'myuniquefield',
            'exclude' => array(
                'field' => 'id',
                'value' => CURRENT_ENTITY_ID,
            ),
        ),
    ),
)

But I don’t know how to inject the requested entity’s ID into the options array. Wouldn’t it make sense, to have Apigility set these excludes for PUT/PATCH requests automatically?


Originally posted by @intellent at https://github.com/zfcampus/zf-apigility/issues/181

weierophinney commented 4 years ago

This CURRENT_ENTITY_ID is inside the route parameters of your RouteMatch. A route identifier does not belong inside the validator/input filter logic. You can make your own validator class where you inject the RouteMatch instance or you can move validation to your ResourceListener class and do the validation after your resolved your object from the database in your patch or update method.


Originally posted by @Wilt at https://github.com/zfcampus/zf-apigility/issues/181#issuecomment-262529354

weierophinney commented 4 years ago

I’ve solved it with the event listener for the time being. But this seems very cumbersome to me.

Here’s a simple example that expects you to have a route like /entities/:id and modifies the NoRecordExists validator of a unique field named 'alias'.

Put this in your Module.php:

public function onBootstrap(MvcEvent $e)
{
    $serviceManager = $e->getApplication()->getServiceManager();

    $events = $serviceManager->get('ZF\ContentValidation\ContentValidationListener')->getEventManager();

    $events->attach('contentvalidation.beforevalidate', function(MvcEvent $e)
    {
        $inputFilter = $e->getParam('ZF\ContentValidation\InputFilter');

        $alias = $inputFilter->get('alias');
        $validatorChain = $alias->getValidatorChain();

        foreach ($validatorChain->getValidators() as $validator) {
            if (! $validator['instance'] instanceof NoRecordExists) {
                continue;
            }

            $validator['instance']->setExclude(array(
                'field' => 'id',
                'value' => intval($e->getRouteMatch()->getParam('id')),
            ));
            break;
        }
    });
}

Originally posted by @intellent at https://github.com/zfcampus/zf-apigility/issues/181#issuecomment-268845010

BeeDi commented 4 years ago

Hello, I suggest setting up a distinct Validator for POST requests. Might be easier. I've done it numerous times as explained here : https://www.apigility.org/documentation/content-validation/advanced