Yii2: Add an ImmutableValidator

Created on 11 May 2017  ·  10Comments  ·  Source: yiisoft/yii2

I want an ImmutableValidator for activerecord.

The validator can stop an attribute of an model being modified when updating the model.

Most helpful comment

Lol @rossoneri, I met the case...I had the same requirement, was checking google and found this. While I absolutely agree with @cebe in the context of a webapp/form situation, where you can control what fields you display to the user, I have a use case where such a validator would make very much sense indeed.

Use case
I am developing a Rest-API and expose an update end point. Now when I would use an unsafe-validator, the illegal try to update a field (in my case the PK) would be silently ignored. To notify the user/client about that I need to add an error to the attribute (in my case $model->id). This in turn will be catched by my biz-layer and throw an exception to the client. Yes, I could throw the exception in the biz-layer, but that's not the place where such logic belongs. Data logic concerning validation of model data belongs into the model.
Furthermore this could also be applied on fields which sould only be changed implicitly in a given scenario on certain fields. A perfect example would be a state-field which is rewound and advanced automatically.

Example using an existing validator

[['id'], function ($attribute, $params) {
    if (!$this->isNewRecord && $this->isAttributeChanged('id')) {
        $msg = Yii::t('app', 'Attribute {attr} is immutable and not be changed after creation', [
            'attr'=>$attribute
        ]);
        $this->addError($attribute, $msg);
    }
}],

Desired functionality 😏

[['id'], 'immutable'],

Exemplary implementation
Just to give an idea what I mean. This is of course limited to ActiveRecord-instances since dirty attributes are not tracked within regular models. A clean and meaningful implementation would also include a base class for validators, which are limited to ActiveRecords only and than extend that one. The base class would perform the check seen below.


ImmutableValidator.php

<?php
namespace app\components;

use Yii;
use yii\base\NotSupportedException;

class ImmutableValidator extends \yii\validators\Validator
{

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        if ($this->message === null) {
            $this->message = Yii::t('app', 'Attribute {attribute} is immutable and can not be changed after creation.');
        }
    }

    /**
     * @inheritdoc
     */
    public function validateAttribute($model, $attribute)
    {
        if (!($model instanceof \yii\db\ActiveRecord)) {
            static::throwNotSupportedException();
        }

        if (!$model->isNewRecord && $model->isAttributeChanged($attribute)) {
            $this->addError($model, $attribute, $this->message);
        }
    }

    /**
     * @inheritdoc
     */
    public function validate($value, &$error = null)
    {
        static::throwNotSupportedException();
    }

    /**
     * Throws a not supported exception
     *
     * @throws \yii\base\NotSupportedException
     */
    protected static function throwNotSupportedException()
    {
        $msg = Yii::t('app', 'ImmutableValidator can only be used on ActiveRecord-instances');
        throw new NotSupportedException($msg);
    }

}

All 10 comments

Can't see any sence in this.
If you wish particular attribute not being populated from user request, you should make it unsafe.

@klimov-paul I'm not to stop attribute being populated from user request, I just want to stop the attribute being modified. But the attribute can also be populated from user when creating a model, in this scene, I don't think I should set the attribute unsafe.

Besides, this immutable validator can applies to many scenarios, in other scenarios, you can still modify the attribute. If you set the attribute unsafe, you can't make it works.

Did you review my code? Please think it again!

I'm not to stop attribute being populated from user request, I just want to stop the attribute being modified.

there is no difference between that.

But the attribute can also be populated from user when creating a model, in this scene, I don't think I should set the attribute unsafe.

if you have different cases where in one it is safe and it is unsafe in the other, you should use scenarios.

I'm not to stop attribute being populated from user request, I just want to stop the attribute being modified.
there is no difference between that.

@cebe If I want to stop attribute being populated from user request, I will not submit it. But in some case, I want the attribute to be shown in model form, and can be submitted, but can not be modified.

Also consider that the attribute has some other validators, how can I set it unsafe conveniently?

@rossoneri your use case sounds very special, you can use your validator if you have those needs but I do not think this is something that fits for the framework.

@cebe ok! I will keep it myself, hope you remind this issue when you meet my case one day, 😀

@rossoneri

public function rules () {
    return [
        ['attribute', 'safe', 'on' => 'create-scenario'],
        ['!attribute', 'safe', 'on' => 'update-scenario'], // notice the ! symbol
    ];
}

then on your controller

$model->scenario = 'create-scenario';
$model->load(['attribute' => 'value']); // will set value

$model->scenario = 'update-scenario';
$model->load(['attribute' => 'new value']); // will not set value
echo $model->attribute; // will echo 'value'

@Faryshta thanks!

Lol @rossoneri, I met the case...I had the same requirement, was checking google and found this. While I absolutely agree with @cebe in the context of a webapp/form situation, where you can control what fields you display to the user, I have a use case where such a validator would make very much sense indeed.

Use case
I am developing a Rest-API and expose an update end point. Now when I would use an unsafe-validator, the illegal try to update a field (in my case the PK) would be silently ignored. To notify the user/client about that I need to add an error to the attribute (in my case $model->id). This in turn will be catched by my biz-layer and throw an exception to the client. Yes, I could throw the exception in the biz-layer, but that's not the place where such logic belongs. Data logic concerning validation of model data belongs into the model.
Furthermore this could also be applied on fields which sould only be changed implicitly in a given scenario on certain fields. A perfect example would be a state-field which is rewound and advanced automatically.

Example using an existing validator

[['id'], function ($attribute, $params) {
    if (!$this->isNewRecord && $this->isAttributeChanged('id')) {
        $msg = Yii::t('app', 'Attribute {attr} is immutable and not be changed after creation', [
            'attr'=>$attribute
        ]);
        $this->addError($attribute, $msg);
    }
}],

Desired functionality 😏

[['id'], 'immutable'],

Exemplary implementation
Just to give an idea what I mean. This is of course limited to ActiveRecord-instances since dirty attributes are not tracked within regular models. A clean and meaningful implementation would also include a base class for validators, which are limited to ActiveRecords only and than extend that one. The base class would perform the check seen below.


ImmutableValidator.php

<?php
namespace app\components;

use Yii;
use yii\base\NotSupportedException;

class ImmutableValidator extends \yii\validators\Validator
{

    /**
     * @inheritdoc
     */
    public function init()
    {
        parent::init();
        if ($this->message === null) {
            $this->message = Yii::t('app', 'Attribute {attribute} is immutable and can not be changed after creation.');
        }
    }

    /**
     * @inheritdoc
     */
    public function validateAttribute($model, $attribute)
    {
        if (!($model instanceof \yii\db\ActiveRecord)) {
            static::throwNotSupportedException();
        }

        if (!$model->isNewRecord && $model->isAttributeChanged($attribute)) {
            $this->addError($model, $attribute, $this->message);
        }
    }

    /**
     * @inheritdoc
     */
    public function validate($value, &$error = null)
    {
        static::throwNotSupportedException();
    }

    /**
     * Throws a not supported exception
     *
     * @throws \yii\base\NotSupportedException
     */
    protected static function throwNotSupportedException()
    {
        $msg = Yii::t('app', 'ImmutableValidator can only be used on ActiveRecord-instances');
        throw new NotSupportedException($msg);
    }

}

@pasci84 Agreed!

Immutable attribute is a very common scene for me in my place, and I really hope this validator can be built-in with Yii Framework.

Was this page helpful?
0 / 5 - 0 ratings