Ignore validation on soft deleted models - yii2

I have user table. I created a form with 3 fields:
Username
phonenumber
status
The first two fields are unique. Model rules for those fields look like this:
[['Username', 'phonenumber'], 'required'],
[['Username', 'phonenumber'], 'unique'],
I use soft deletion, so when record is deleted, it actually stays in database but status value will change to 0.
The problem is, if I add a record with existing username it shows an error message like "already added". I need to ignore validation if username have a status with value 0.

Use filter property of UniqueValidator
public function rules()
{
return [
...
['username', 'unique', 'filter' => ['<>', 'status', 0]];
...
];
}
It's better to declare constant instead of 0 (something like const STATUS_DELETED = 0) and user it as self::STATUS_DELETED inside of User class. Also you can use != instead of <>.
The last recommendation will be to use username instead of Username to follow convention of naming database table columns.
Read more about ways of declaring filter in official docs.
The ways of setting filter condition as array is described here.

You can use your own function to decide the given username already exists in active status or not. Use this function in "when" property of your unique validation rule.
Have a look :
public function rules()
{
$check = function($model) {
$existActiveUser = User::model()->findByAttributes(array("username"=>$model->username,"status"=>1));
if($existActiveUser)
return true;
else
return false;
};
return [
['Username', 'phonenumber'], 'required'],
[['Username','phonenumber'],'unique','when'=>$check],
}

Related

Date condition (visits)

I'm trying to make a condition where if the registry has the same the same image as the same date cannot be saved
public function validateVisit()
{
if (Visit::find()->where(['date_visit'=>$this->date_visit])->all()) {
if (Visit::find()->where(['imagen_id'=>$this->imagen_id])->all()) {
$this->addError('imagen_id', 'Already exists this visit within the range.');
}
}
}
In my code it does not save when the image that is entered already exists in the database, in my case, I require that you do not save when the same date that is entered with the same image already exists
You can use unique validator for this.
public function rules()
{
return [
[['imagen_id'], 'unique', 'targetAttribute' => ['imagen_id', 'date_visit']],
// ... other validations ...
];
}
The first item in rule definition ['date_visit'] says to what attribute the error will be set. The targetAttribute define combination of attributes that must be unique. In this case the validation will only pass when the combination of imagen_id and date_visit attributes doesn't exist.
See more about unique validator.

Integers are marked as dirty attributes no matter what

I need to check if a model has been updated and what attributes have changed when saving.
I'm using dirtyAttributes and filter intval as the docs suggests.
The values are coming from an API and are type-cast as they come in, so in theory the filter is redundant.
Model rules
public function rules()
{
return [
[['contract_date', 'order_date'], 'integer'],
[['contract_date', 'order_date'], 'filter', 'filter' => 'intval'],
];
}
This is some of the code currently running:
// Add the changed status variables to the job log
$dirty_attributes = array_keys($model->dirtyAttributes);
if($model->save()) foreach ($dirty_attributes as $attribute)
{
$data[$attribute] = $model->getOldAttribute($attribute).' ('.gettype($model->getOldAttribute($attribute)).')'. ' => '. $model->$attribute.' ('.gettype($model->$attribute).')';
}
var_dump($data);
This produces:
["contract_date"]=>
string(44) "1559669638 (integer) => 1559669638 (integer)"
["order_date"]=>
string(44) "1559669638 (integer) => 1559669638 (integer)"
There is probably something obvious I'm missing, but I can understand what.
After saving model all "oldAttributes" are updated to store new values so comparing them like you do makes no sense. If you want to check which attributes have been changed after saving you can override afterSave() method in your model like:
public function afterSave($insert, $changedAttributes)
{
// $changedAttributes -> this is it
parent::afterSave(); // call parent to trigger event
}
or listen for ActiveRecord::EVENT_AFTER_INSERT / ActiveRecord::EVENT_AFTER_UPDATE event where this data is also passed.

Yii2: how to perfect separate validate model and save model?

In which case is it better to use the Validate model? Example: I have two model
AgreementForm
Agreement
When I create data - I use
$model = new AgreementForm();
if ( $model->load( \Yii::$app->request->post() ) && $model->save() ) {
....
}
Code of AgreemtnForm
AgreementForm extemds Model {
......
public function save() {
if(!$this->validate()) {
return null;
}
$model = new Agreement();
$model->content = $this->content;
if( $model->save() ) {
return true;
}
}
But when I update data, I use only that code:
public function actionUpdate( $id ) {
$model = Agreement::findOne( $id );
if( $model->load( \Yii::$app->request->post() ) && $model->save() ){
$this->refresh();
}
else {
return $this->render('update', [
'model' => $model,
]);
}
}
What is the validation model in this case, if I duplicate the validation in another model? Can I only use 1 model?
I will try to explain why you should almost always use a Form model to validate your data, providing some cases:
1) Doing
if( $model->load( \Yii::$app->request->post() ) && $model->save() )
could be dangerous, the user can send post data that he was not supposed to.
eg. Agreement has a userid column that gets id of the user that created it. If a user, sends this field in the post request, he could potentially change its value.
Your form model should define the properties that you expect to be sent.
2) You want to define additional validation for your model based on eg. the user role, the time the Agreement is valid etc.
Lets say you have 2 user roles:
Retailer
Merchant
Retailer can create an Agreement that is 300 characters long and max price 1000, opposed to 700 and 10000 for the merchant.
How do you cope with that?
You create 2 different forms:
$user = Yii::$app->user;
if ($user->can('retailer')) {
$model = new RetailerAgreementForm();
}
else {
$model = new MerchantAgreementForm();
}
if ( $model->load( \Yii::$app->request->post() ) && $model->save() ) {
....
}
In the form models, you can add the additional validation for your fields:
public function rules()
{
return [
['body', 'string', 'max' => 300],
['price', 'integer', 'max'=> 1000],
];
}
Using different forms for this I believe is the best option.
3) Your form model fields do not correspond to Database model columns 1 to 1.
Consider this example:
Your want to save the address of the agreement, street, state and city. You have a list of cities, states and streets.
The only thing you want to do with the address is save it and load it, eg. nobody is going to search per city.
So you just define a column address(type text) in your table and save the data as a JSON.
Your AgreementForm defines the address as separate fields and validates them accordingly and your Agreement model just validates address to be a string.
Do not duplicate your validation between AgreementForm and Agreement models. They should define different validation rules.
Note: Even if you do not render the form in a view and just post some data, it is good to use a form model to get exactly the fields you want to change and if needed, validate them with additional rules. The only case that I could think to just use the DB model directly is when you want to provide just some basic crud operations for your model.

Yii2: Non-Scenario fields are not saved

I'm using different scenario for validation purpose.
The field email is not required in a scenario but if i pass email, it is not getting saved in DB.
Rules in Model:
[['firstname','email'], 'string', 'max' => 256],
Scenario function
public function scenarios() {
$scenarios = parent::scenarios();
$scenarios['insert2'] = ['firstname', 'status'];
return $scenarios;
}
In controller:
$model = new User();
$model->scenario = "insert2";
$model->load($data);
print_r($model);
Print_r returns email with empty
From Yii 2 load() docs:
Note, that the data being populated is subject to the safety check by setAttributes().
Now, setAttributes() signature:
public void setAttributes ( $values, $safeOnly = true )
where $safeOnly set to true means the assignments should only be done to the safe attributes. A safe attribute is one that is associated with a validation rule in the current $scenario.
So email must be included in the scenario.
In Yii2, when you want the rule to be applied on certain scenarios, you can specify the on property of a rule, like the following
public function rules()
{
return [
//rule applied only in scenario 'insert2'
[['first_name','status'], 'required','on' => 'insert2'],
//rule applied in all scenarios
['email','safe'],
//rule applied only in scenario 'insert3'
[['first_name','status','email'], 'required','on' => 'insert3'],
];
}
Now if you specify $model->scenario = "insert2" in controller then first_name and status are required and email if you give any value will get saved because ['email','safe'] rule also applied here.
If you specify $model->scenario = "insert3" in controller then first_name,status and email are now required fields and rule ['email','safe'] also get applied.
Please note you may not use public function scenarios() {.. here
From http://www.yiiframework.com/doc-2.0/guide-structure-models.html#validation-rules

Preventing malicious users update data at add action

Here is a basic add action:
public function add()
{
$article = $this->Articles->newEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->data);
if ($this->Articles->save($article)) {
$this->Flash->success('Success.');
return $this->redirect(['action' => 'index']);
} else {
$this->Flash->error('Fail.');
}
}
$this->set(compact('article'));
}
If a malicious user injects at form a field with name id and set the value of this field to 2. Since the user do that the id value will be in $this->request->data so at $this->Articles->patchEntity($article, $this->request->data) this id will be patched and at $this->Articles->save($article) the record 2 will be updated instead of create a new record??
Depends.
Entity::$_accessible
If you baked your models, then this shouldn't happen, as the primary key field will not be included in the entities _accessible property, which defines the fields that can be mass assigned when creating/patching entities. (this behavior changed lately)
If you baked your models, then this shouldn't happen, as the primary key field(s) will be set to be non-assignable in the entities _accessible property, which means that these the fields cannot be set via mass assignment when creating/patching entities.
If you didn't baked your models and haven't defined the _accessible property, or added the primary key field to it, then yes, in case the posted data makes it to the patching mechanism, then that is what will happen, you'll be left with an UPDATE instead of an INSERT.
The Security component
The Security component will prevent form tampering, and reject requests with modified forms. If you'd use it, then the form data wouldn't make it to the add() method in the first place.
There's also the fieldList option
The fieldList option can be used when creating/patching entities in order to specifiy the fields that are allowed to be set on the entity. Sparse out the id field, and it cannot be injected anymore.
$article = $this->Articles->patchEntity($article, $this->request->data, [
'fieldList' => [
'title',
'body',
//...
]
]);
And finally, validation
Validation can prevent injections too, however that might be considered a little wonky. A custom rule that simply returns false would for example do it, you could create an additional validator, something like
public function validationAdd(Validator $validator) {
return
$this->validationDefault($validator)
->add('id', 'mustNotBePresent', ['rule' => function() {
return false;
}]);
}
which could then be used when patching the entity like
$article = $this->Articles->patchEntity($article, $this->request->data, [
'validate' => 'add'
]);