zfcampus / zf-apigility

BSD 3-Clause "New" or "Revised" License
258 stars 52 forks source link

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

Open intellent opened 7 years ago

intellent commented 7 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?

Wilt commented 7 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.

intellent commented 7 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;
        }
    });
}
weierophinney commented 4 years ago

This repository has been closed and moved to laminas-api-tools/api-tools; a new issue has been opened at https://github.com/laminas-api-tools/api-tools/issues/12.