Yii 1.1: composite-unique-key-validatable

Behavior that attaches methods for validation of composite unique keys of AR-models
15 followers

To validate composite unique keys attach the ECompositeUniqueKeyValidateable behavior, declare unique keys and declare short validation method in a model class.

There are few reasons to use behavior and validation method instead of writing validation class

  • we can't attach a handler for CActiveRecord::onAfterFind() with only validator (this is needed for storing of old attributes of the model for proper validation when updating an existing record)

  • CValidator doesn't imply validation of several attributes

Requirements

Tested on Yii 1.1 and php 5.3

Usage

Attach the ECompositeUniqueKeyValidatable behavior and declare unique keys

public function behaviors() {
        return array(
            'ECompositeUniqueKeyValidatable' => array(
                'class' => 'ECompositeUniqueKeyValidatable',
                'uniqueKeys' => array(
                    'attributes' => 'login, applicationId',
                    'errorMessage' => 'Your login is already taken'
                )
            ),
        );
    }

declare simple validation method in the model class

/**
     * Validates composite unique keys
     *
     * Validates composite unique keys declared in the
     * ECompositeUniqueKeyValidatable bahavior
     */
    public function compositeUniqueKeysValidator() {
        $this->validateCompositeUniqueKeys();
    }

declare the validation rule

public function rules() {
        return array(
            // the first parameter doesn't matter, I use '*' (pretty ugly
            // definition, but I don't know a better way)
            array('*', 'compositeUniqueKeysValidator'),
        );
    }

Description of the options of unique keys

  • attributes - unique key

  • errorMessage - error message

  • errorAttributes (optional) - attributes of the model which will contain the error message

  • skipOnErrorIn (optional) - if one of this attributes contains errors then validation will be skipped

Some examples

declaring of two composite unique keys

public function behaviors() {
        return array(
            'ECompositeUniqueKeyValidatable' => array(
                'class' => 'ECompositeUniqueKeyValidatable',
                'uniqueKeys' => array(
                    array(
                        'attributes' => 'email, applicationId',
                        'errorAttributes' => 'email, email_confirmation',
                        'errorMessage' => 'This email is already registered',
                        'skipOnErrorIn' => 'email, applicationId'
                    ),
                    array(
                        'attributes' => 'login, applicationId',
                        'errorAttributes' => 'login',
                        'errorMessage' => 'Your login is already taken',
                        'skipOnErrorIn' => 'login, applicationId'
                    ),
                )
            ),
        // ...

Resources

Total 20 comments

#12524 report it
cephyn at 2013/03/25 07:33pm
Breaks in 5.4

the deprecated "Call-time pass-by-reference" will fail completely in php 5.4

Building on Sebastian K.'s work, I altered:

// convert comma separated lists to arrays
        foreach ($this->uniqueKeys as $key=>$uk) {
 
            $this->uniqueKeys[$key] = $uk; //ADDED
 
            isset($uk['attributes']) or $uk['attributes'] = array();
            is_array($uk['attributes']) or $this->_stringListToArray($this->uniqueKeys[$key]['attributes']);
 
            // *nonexistent attribute* means that an error message will not be attached to a certain attribute
            isset($uk['errorAttributes']) or $uk['errorAttributes'] = array('*nonexistent attribute*');
            is_array($uk['errorAttributes']) or $this->_stringListToArray($this->uniqueKeys[$key]['errorAttributes']);
 
            isset($uk['skipOnErrorIn']) or $uk['skipOnErrorIn'] = array();
            is_array($uk['skipOnErrorIn']) or $this->_stringListToArray($this->uniqueKeys[$key]['skipOnErrorIn']);
 
        }
#7198 report it
ololo at 2012/03/02 07:10pm
RE: PHP 5.3 problems

sieppl, I have just figured out why I don't see "call-time pass-by-reference" warnings: allow_call_time_pass_reference php.ini option should be disabled

now with this option disabled I can see that everything is allright with the extension and I don't use "call-time pass-by-reference" so I reverted back the last commit and updated the extension again

let me clarify

"call-time pass-by-reference" is when we do this

<?php
function do_something($var) {
    // anything
}
$blah = "blah";
do_something(&$blah);

and this is really dangerous to pass something by reference in a function whose argument is not supposed to be passed by reference

see http://php.net/manual/en/ini.core.php

// Passing arguments by reference at function call time was deprecated for code-cleanliness reasons. A function can modify its arguments in an undocumented way if it didn't declare that the argument shall be passed by reference. To prevent side-effects it's better to specify which arguments are passed by reference in the function declaration only.

but when we do something like this then it's OK and there is no warning:

<?php
function do_something(&$var) {
    // anything
}
$blah = "blah";
do_something($blah);
#6973 report it
Sebastian K. at 2012/02/16 10:37am
@ololo

Sorry, I was not precise enough. You are doing a "Call-time pass-by-reference" (inside the for loops) which is deprecated since 5.3: http://php.net/manual/en/migration53.deprecated.php

So actually this is not a "bug", but a problem when your parser raises E_DEPRECATED.

#6972 report it
ololo at 2012/02/16 10:12am
RE: PHP 5.3 problems

could you please describe this problem or provide a php.net link to the bug report or make some example reproducing this problem?

just curious, sometimes people are confused with by-reference loops and call expected behavior "buggy" e.g. https://bugs.php.net/bug.php?id=39307

#6968 report it
Sebastian K. at 2012/02/16 09:18am
PHP 5.3 problems

There are problems with PHP 5.3, when you use the call by reference in combination with for-loops. Following fixes make it work:

/**
     * Normalize unique keys
     */
    private function _normalizeKeysData() {
        //...
 
        // convert comma separated lists to arrays
        foreach ($this->uniqueKeys as $key => $uk) { //CHANGED
 
            //...
 
            $this->uniqueKeys[$key] = $uk; //ADDED
        }
 
         //...
    }

and

private function _stringListToArray(&$list) {
        $list = explode(',', $list);
        foreach ($list as $key => $item) { //CHANGED
            $list[$key] = trim($item); //CHANGED
        }
    }

Please update this extension. Beside this minor issue it works very nice in 5.3, thank you!!!

#6565 report it
jmariani at 2012/01/18 06:12pm
Re: Undefined index: oldValue (on line 90)

Hi.

Thank you for your support. I'm using (http://www.yiiframework.com/extension/multimodelform "multimodel-form") and I'm trying to use your extension with the detail model. I think the problem is there.

I'll investigate and let you know my findings.

Regards.

#6562 report it
ololo at 2012/01/18 03:57pm
Re: Undefined index: oldValue (on line 90)

> Question (since I'm a newbie): Shouldn't everybody implementing
> an event call parent before they proceed?

hm, I thought no, but now I'm in doubt about it

I think you can only break something by overriding afterFind() in your model because only CActiveRecord::afterFind() raises an event (so it have to be called through the parent:: keyword if you want to raise an event), and when it's raised all subscribers (including behaviors) are getting notified and perform their logic independently. So I think it's not about another behavior, but I'm not sure.

Try to define ECompositeUniqueKeyValidateable prior to your another behavior and check if afterFind() handler of another behavior is called.

#6558 report it
jmariani at 2012/01/18 01:42pm
Re: Undefined index: oldValue (on line 90)

Thank you ololo.

Yes, I do use another behavior which implements afterFind.

Should I put parent::afterFind in that behaviour implementation and in yours too?

Question (since I'm a newbie): Shouldn't everybody implementing an event call parent before they proceed?

Regards.

#6557 report it
ololo at 2012/01/18 01:30pm
Re: Undefined index: oldValue (on line 90)

Hi, oldValue should be initialized for each of the unique keys in afterFind() event handler defined in the behavior

So I can assume that ECompositeUniqueKeyValidatable::afterFind() wasn't called, e.g. it can happen because you have overriden afterFind() event handler without calling parent::afterFind().

Anyway, first make sure ECompositeUniqueKeyValidatable::afterFind() is called (for example by putting die('smth') in the afterFind() method of the behavior)

#6556 report it
jmariani at 2012/01/18 12:44pm
Undefined index: oldValue (on line 90)

Hi.

I', having this error while testing your extension.

Any idea?

Regards.

#5294 report it
Srinivasan at 2011/10/02 12:09am
Re: Problem when update (to ololo)

I got one thing

The problem occur only when I use dependent dropdown boxes. (Working fine in normal cause).

Fields are in Form
1) Road_id - dropdown box
2) Street - text box
composite unique keys are ROAD_ID and STREET

In this cause, your extension works as expected. Its excellent

But

Fields are in Form
1) City_id - Dependent Dropdown
2) Area_id - Dependent Dropdown
3) Road_id - dependent dropdown box
4) Street - text box
composite unique keys are ROAD_ID and STREET

In this cause, we are facing problem when update without any change

I will send you the code by mail If you want

Thank you very much

#5289 report it
ololo at 2011/10/01 11:05am
Re: Problem when update

Hm, sorry, but I have no idea what can cause this problem. If you provide me some more information so that I can reproduce the problem, then I'll try to help.

#5288 report it
Srinivasan at 2011/10/01 10:12am
Re: Problem when update (to ololo)

No records as you told. I checked. Got the same error even when I try to update (without any changes) which I create just now

#5255 report it
ololo at 2011/09/26 04:55pm
2 chennaiiq

Hi, make sure that there is no other records with the same unique key as the record you are updating (that can happen if you had created several records before you implemented validation).

#5249 report it
Srinivasan at 2011/09/26 10:34am
Problem when update

I got unique error when I try to update a record without changing values of the record. I think its treat as a new record.

#4839 report it
ololo at 2011/08/20 04:03pm
> Where do you extract the file to?

anywhere you want, you can use path alias in behavior definition or rely on autoload

public function behaviors() {
        return array(
            'ECompositeUniqueKeyValidatable' => array(
                'class' => 'application.extension.ECompositeUniqueKeyValidatable',
#4664 report it
Tommo at 2011/08/01 08:36pm
Where do you extract the file to?

Also, where does the behaviors method go?

#4504 report it
redguy at 2011/07/14 04:57am
compositeUniqueKeysValidator

then you could create switch "useValidationEvent" which will allow to choose between hidden usage of onBeforeValidation/onAfterValidation (default) and disable this feature to use explicit validation.

...as you can see I prefer solution "less code and modyfications is better in most cases, but let there be a switch to fully manual mode for advanced scenarios" ;-)

#4179 report it
ololo at 2011/06/13 02:22pm
compositeUniqueKeysValidator

yes, but in this case users will not be able to set order in which validators should be performed, and undeclared validation in onAfterValidation() method looks messy to me, I prefer more explicit ways

#4176 report it
redguy at 2011/06/13 04:03am
compositeUniqueKeysValidator

maybe you could use onBeforeValidate or onAfterValidate events instead of making users to provide their own wrapper of your mechanizm (compositeUniqueKeysValidator)?

Leave a comment

Please to leave your comment.

Create extension