Fire Validation Rule Only If Attribute's Value Has Changed

I need to implement following scenario:

  • validate particular attribute, only if it’s value is changed and/or
  • don’t perform validation, if particular form’s field is hidden or disabled.

What is the easiest / best way to implement this?

For checking, if value has changed, the only thing, I figured out, is to query DB (in custom validator or beforeValidate() event) and compare model’s supplied value with the one stored in the database. But this seems to be crazy (additional DB’s query?), so I’m thinking, if there is a better way of doing this?

For second (performing validation only, if field isn’t hidden or disabled), I tried to use unsave validator:


public function rules()

{

	return array

	(

    	array('level', 'unsafe', 'on'=>'update'),

    	array('level', 'validateUser')

	);

}

But I failed. My custom validator (validateUser) is still fired, so I must be missing something obvious.

You can prevent an attribute from being massively assigned (or validated) by simply omitting rules for it in the specific scenario.

For instance, if the current scenario is ‘update’ and you have no rules defined for the ‘level’ attribute which apply in that scenario, that attribute will not be changed when you perform $model->attributes = $_POST[‘Model’].

This means that ‘level’ should only have rules defined for the specific scenarios in which it can be updated. It should not appear in rules where the scenario is ‘update’ or the scenario is not specified.

I don’t know of a best-practise way to handle validation of only those attributes which have changed, but there are several ways you could tackle it. Bear in mind that before you assign the attributes to the model from the form, you have the original values (in the model) and the new values (in the POST array), so you could create a method to determine which attributes have changed.

All fine and understood. The only addition is, that your approach would require to move checking (validation) outside model, back to controller and action.

This breaks the idea of MVC (all model-related validation should be done in model itself), but I see no other way (except mentioned additional DB query, which is wrong) to have both value sets (original and modified in form) at model’s validator level.

This is fine with me. The idea of doing this special-case checking before $model->attributes = $_POST does convince me. Thanks.

Perhaps you could so something like the following in a base class of your models, or in a behavior if you modify it slightly:

[s]




    public function saveChangedAttributes($newValues)

    {

        $changedValues = array();


        // Build array of changed values based on non-strict comparison

        foreach ($newValues as $key => $value)

        {

            if (property_exists($this, $key) && $this->$key != $value)

                $changedValues[$key] = $value;

        }


        // Only assign changed values to model's attributes

        $this->attributes = $changedValues;


        // Only validate and save changed values

        return $this->save(true, array_keys($changedValues));

    }



[/s]

Then in the controller, you could call it like this:




    if (isset($_POST['YourModel']))

    {

        if ($model->saveChangedAttributes($_POST['YourModel']))

        {

            // etc

        }

    }



Not at all tested :)

It shouldn’t reduce security because it still relies on Yii’s methods to assign the attributes and to save the model. The non-strict comparison might cause issues though, as it might think fields haven’t changed when they have (such as an empty field changing to ‘0’).

EDIT:

The property_exists function can’t detect properties which are made available through __get magic methods, so the following might work better




    public function saveChangedAttributes($newValues)

    {

        $changedValues = array();

        $attributes = $this->getAttributes();


        // Build array of changed values based on non-strict comparison

        foreach ($newValues as $key => $value)

        {

            if (array_key_exists($key, $attributes) && $attributes[$key] != $value)

                $changedValues[$key] = $value;

        }


        // Only assign changed values to model's attributes

        $this->attributes = $changedValues;


        // Only validate and save changed values

        return $this->save(true, array_keys($changedValues));

    }



Great idea! And what do you think about mine?

Override base model’s setAttributes method and implement simple addition, that would store original model’s values in some array before overwriting them with the one comming from a form?

Then, anyone could get current value using standard approach of: $model->property and previous one from for example $model->previousValues[‘property’]. Since, it would be stored directly in each model, could be easy accessible in any custom validator.

This would be available only per current session / request, when controller is processing form data.

Or, we can even consider going further and doing the comparison right, when an assignment to attributes is made and offer user other or another array, containing only true/false values, if particular property has the same value as previously set: $model->valueChanged[‘property’].

This way, we could have full access to determine, which (if) model’s properties has changed and implementation of validating / saving only changed properties would be at our hands. And – what is most important – without extra query to the database!

That’s reminiscent of my solution for history logging. It’s implemented as a behavior and stores a copy of the original attributes in an array in onAfterFind(). When the user saves the record it compares the model values with those in the array and only writes the changes to the history table. On deletion, all of the model’s original attributes are logged.

Perhaps you could do something similar and provide your functionality in a behavior, so you can selectively add it to your models.

If it helps, I stumbled upon the need to ‘tell’ if an attribute “is dirty” back then and implemented it this way:

  • In the model AR class [b]afterFind/b method I assign all loaded values to $this->originalAttributes
  • I’ve created isAttributeDirty(att_name) which gets an attribute name and compares the original value with the possible updated one, and return a boolean accordingly.

You can see it all in the extension I released back then, a base extension which have also other goodies (although I must note that I would have changed it today) - PcBaseArModel.

Sounds like a great idea! Thanks (+1)!

EDIT: There’s this Wiki article, about a different purpose (logging changes to Active Record), which shows your idea as an example – assigning current Active Record attributes to setOldAttributes in afterFind() and using them to “detect” only “dirty” attributes, when logging AR’s changes to database.