5

Im learning doctrine and i have a form. ZF2 and doctrine with field "email". This field need to be unique, so i need validator for it. Im using fieldsets too (its important here). Problem is, when i use:

DoctrineModule\Validator\UniqueObject

it is impossible to create new entity. This validator need primary key to compare. Validator dump error with message:

Expected context to contain itemId

itemId is my primary key.

So its obvious, that i need to use UniqueObject for update, and:

DoctrineModule\Validator\NoObjectExists

for new entity. And question is:

What is the best way, to store different input filter specification for existing and new entity?

Or, better if possible: How to use Unique validator with new, and existing records with zend form fieldsets.

If i put it in the form, I need to modify it inside controller if entity is new or no. Not so good idea.

I think that the best way is to store input filter spec. inside entity repository, but how i can check there if entity is new or not?

---- edit

I saw documentation, i know how to use unique object but i have error like said before: "Expected context to contain itemId". I think problem is with fieldsets (im using it). I dont understand how to do this (text from docs):

If you leave out the use_context option or set it to false you have to pass an array containing the fields- and identifier-values into isValid(). When using Zend\Form this behaviour is needed if you're using fieldsets.

Ok im using fieldsets, so now what i can do? How i can pass correct values to isValid when im using zend forms?

Daimos
  • 1,473
  • 10
  • 28
  • 1
    Check out my answer about this subject [here](http://stackoverflow.com/a/16812388/199593). – edigu Nov 05 '15 at 14:09

4 Answers4

2

With UniqueObject validator you need to have the identifier field in the context. So it will only work if the email column is the identifier column of your Email entity? You have an additional id column. It would be better to use the NoObjectExists validator in your user case:

'email' => array(
    'validators' => array(
        array(
            'name' => 'DoctrineModule\Validator\NoObjectExists',
            'options' => array(
                'object_repository' => $entityManager->getRepository(
                    'Application\Entity\Email'
                ),
                'fields' => 'email'
            )
        )
    )
)

You can also find this example in the documentation.

EDIT

About separating logic for update and new input filters I would suggest making two folders. It is better to keep them strictly separated like that, otherwise it is very likely that mistakes will happen. You could for example do like this (but it totally depends on you personal preference how this is organized).

Application/InputFilter/Create
    UserInputFilter

And one for updating resources:

Application/InputFilter/Update
    UserInputFilter

And then in your controller you could do like this:

<?php

namespace Application\Controller;

use Application\InputFilter\Create;
use Application\InputFilter\Update;

class UserController{

    public function updateAction()
    {
        $inputFilter = new Update\UserInputFilter();
        //... etc
    }

    public function createAction()
    {
        $inputFilter = new Create\UserInputFilter();
        //... etc
    }
}
Wilt
  • 41,477
  • 12
  • 152
  • 203
  • Because of context problems with fieldset i using it. Cant see other way. But question was "What is the best way, to store different input filter specification for existing and new entity?" – Daimos Nov 02 '15 at 11:24
  • @Daimos This depends a lot on your personal preference. There are many ways to do this, but I added an example to my answer... – Wilt Nov 03 '15 at 08:31
  • Thanks for that, and what do you think about storing input filter in doctrine repository? I saw a lot examples with that, but its hard to get information if its new repo or no – Daimos Nov 03 '15 at 08:59
  • 1
    @Daimos, I don't think you should clutter your repositories with all kinds of logic like that. Code related to `InputFilter` does not belong there if you ask me. Same as people adding input filter config to their entities; I don't like it. But again, there are no strict guidelines on how to do this. It is a personal preference. – Wilt Nov 03 '15 at 09:09
2

I developed it for my application to deal with this issue..hope it will help

<?php

namespace Application\Validator;

use Zend\Validator\AbstractValidator;

class DbUniqueObject extends AbstractValidator {

const INVALID = 'objectAlreadyExists';

protected $messageTemplates = array(
    self::INVALID => "Field value must be unique in the database (id=%id%)",
);
protected $messageVariables = array(
    'id' => array('options' => 'id'),
);
protected $options          = array(
    'em',
    'entity',
    'field',
    'exclude_id'
);

public function __construct($options = null) {
    $this->options = $options;
    parent::__construct($options);
}

public function isValid($value) {
    $qb = $this->em->createQueryBuilder();
    $qb->select('t')
            ->from($this->entity, 't')
            ->where('t.' . $this->field . '= :field')
            ->setParameter('field', $value);

    if (boolval($this->exclude_id)) {
        $qb->andWhere('t.id <> :id');
        $qb->setParameter('id', $this->exclude_id);
    }
    $result = $qb->getQuery()->getResult();
    if (boolval($result)) {
        $this->options['id'] = $result[0]->getID();
        $this->error(self::INVALID);
        return false;
    }
    return true;
}

public function __get($property) {
    return array_key_exists($property, $this->options) ? $this->options[$property] : parent::__get($property);
}

}

Anf then in your input filter attached to your form, just add ,in the 'Validators' array :

                    'validators' => array(
                    array(
                        'name'    => '\Application\Validator\DbUniqueObject',
                        'options' => array(
                            'em'         => $em, //Entity manager
                            'entity'     => 'Application\Entity\Customer', // Entity name
                            'field'      => 'label', // column name
                            'exclude_id' => $this->customer->getID() : null, // id to exclude (useful in case of editing)
                        )
                    )
                ),
Seif
  • 51
  • 1
  • 6
  • Nice solution, thank you. Im bind entity object to the form, so i can pass it to the validator, check if its new or not and i dont need additional query. But still own validator is good way to go, my way for now is to switch ObjectExist and Unique for new and existing objects – Daimos Nov 04 '15 at 18:36
  • Could give you +10 ;) exactly what I was looking for – cwhisperer Feb 07 '18 at 07:12
1

I will show my solution, i think quite good if somebody want to keep validator inside form. In my fieldset i have method getInputFilterSpecification(), which is processed automatically, and there:

public function getInputFilterSpecification()
{
    // im bind object new or exist to the form, 
    // so there is a simple way to get it:
    $entity = $this->getObject(); 
    // method to check if object is new or not:
    // $this->_entityManager i have entitymanager passed in constructor
    switch ($this->_entityManager->getUnitOfWork()->getEntityState($entity)) {
        case \Doctrine\ORM\UnitOfWork::STATE_MANAGED:
            // im switch validator, unique for existing:
            $existValidator = 'DoctrineModule\Validator\UniqueObject';
        case \Doctrine\ORM\UnitOfWork::STATE_NEW:
            $existValidator = 'DoctrineModule\Validator\NoObjectExists';
    }
    // propably we can also check if object primary key is empty or not
    // i will test it later

    return array(
        'elementName' => array(
            'required' => true,
            'validators' => array(
                array(
                    'name' => $existValidator,
                    'options' => array(
                        'object_repository' => $this->_entityManager->getRepository('My\Entity'),
                        'object_manager' => $this->_entityManager,
                        'fields' => 'fieldName',
                    )
                )
            )
        )
    );
}

Its still not tested with unique validator. Maybe there will be problem with primary key too, i will check it in few days. But its still simple way to assign correct validator based on new or existing node.

My conclusion: Unique validator dont work with ZF2 and fieldsets.

Daimos
  • 1,473
  • 10
  • 28
0

You need only to have UniqueObject validator as the following in the form Input Filter Specification:

    'email' => array(
        'validators' => array(
            array(
                'name' => 'DoctrineModule\Validator\UniqueObject',
                'options' => array(
                    'use_context'       => true,
                    'object_repository' => $this->objectManager->getRepository('Namespace\EntityName'),
                    'object_manager' => $this->objectManager,
                    'fields' => 'email',
                    'messages' => array(
                        'objectNotUnique' => 'Email already exists!'
                    ),
                ),
            )
        ),
    ),

You can find more details from this link: https://github.com/doctrine/DoctrineModule/blob/master/docs/validator.md

Mohammad ZeinEddin
  • 1,608
  • 14
  • 17
  • there is error with UniqueObject on new records. Need primary id key to work – Daimos Oct 31 '15 at 18:28
  • What exactly is the error? I am using the above code and it work for both new and existing records! – Mohammad ZeinEddin Nov 01 '15 at 08:12
  • I edited my question, error is: Expected context to contain itemId. And im tryied with use_context false/true – Daimos Nov 01 '15 at 12:13
  • I think the problem is with fieldsets (im using it). But i created id field inside main fieldset, main form, same fieldset where i use validator and still same error. From manual i see that i need to pass two values if im using fieldset, but i have no idea how to do that – Daimos Nov 01 '15 at 23:08