Giter VIP home page Giter VIP logo

yii2-save-relations-behavior's Introduction

Yii2 Active Record Save Relations Behavior

Automatically validate and save related Active Record models.

Latest Stable Version Total Downloads Code Coverage Build Status Latest Unstable Version License

Features

  • Both hasMany() and hasOne() relations are supported
  • Works with existing as well as new related models
  • Compound primary keys are supported
  • Only pure Active Record API is used so it should work with any DB driver
  • As of 1.5.0 release, related records can now be deleted along with the main model
  • ⚠️ As of 2.0.0 release, relations attributes now honor the safe validation rule

Installation

The preferred way to install this extension is through composer.

Either run

php composer.phar require --prefer-dist la-haute-societe/yii2-save-relations-behavior "*"

or add

"la-haute-societe/yii2-save-relations-behavior": "*"

to the require section of your composer.json file.

Configuring

Configure model as follows

use lhs\Yii2SaveRelationsBehavior\SaveRelationsBehavior;

class Project extends \yii\db\ActiveRecord
{
    use SaveRelationsTrait; // Optional

    public function behaviors()
    {
        return [
            'timestamp'     => TimestampBehavior::className(),
            'blameable'     => BlameableBehavior::className(),
            ...
            'saveRelations' => [
                'class'     => SaveRelationsBehavior::className(),
                'relations' => [
                    'company',
                    'users',
                    'projectLinks' => ['cascadeDelete' => true],
                    'tags'  => [
                        'extraColumns' => function ($model) {
                            /** @var $model Tag */
                            return [
                                'order' => $model->order
                            ];
                        }
                    ]
                ],
            ],
        ];
    }

    public function rules()
    {
        return [
            [['name', 'company_id'], 'required'],
            [['name'], 'unique', 'targetAttribute' => ['company_id', 'name']],
            [['company', 'users', 'projectLinks', 'tags'], 'safe']
        ];
    }

    public function transactions()
    {
        return [
            self::SCENARIO_DEFAULT => self::OP_ALL,
        ];
    }

    ...


    /**
     * @return ActiveQuery
     */
    public function getCompany()
    {
        return $this->hasOne(Company::className(), ['id' => 'company_id']);
    }

    /**
     * @return ActiveQuery
     */
    public function getProjectUsers()
    {
        return $this->hasMany(ProjectUser::className(), ['project_id' => 'id']);
    }

    /**
     * @return ActiveQuery
     */
    public function getUsers()
    {
        return $this->hasMany(User::className(), ['id' => 'user_id'])->via('ProjectUsers');
    }

    /**
     * @return ActiveQuery
     */
    public function getProjectLinks()
    {
        return $this->hasMany(ProjectLink::className(), ['project_id' => 'id']);
    }

    /**
     * @return ActiveQuery
     */
    public function getTags()
    {
        return $this->hasMany(Tag::className(), ['id' => 'tag_id'])->viaTable('ProjectTags', ['project_id' => 'id']);
    }
}

Though not mandatory, it is highly recommended to activate the transactions for the owner model. ⚠️ Relations attributes has to be defined as safe in owner model validation rules in order to be saved.

Usage

Every declared relations in the relations behavior parameter can now be set and saved as follow:

$project = new Project();
$project->name = "New project";
$project->company = Company::findOne(2);
$project->users = User::findAll([1,3]);
$project->save();

You can set related model by only specifying its primary key:

$project = new Project();
$project->name = "Another project";
$project->company = 2;
$project->users = [1,3];
$project->save();

You can even set related models as associative arrays like this:

$project = Project::findOne(1);
$project->company = ['name' => 'GiHub', 'description' => 'Awesome']; // Will create a new company record
// $project->company = ['id' => 3, 'name' => 'GiHub', 'description' => 'Awesome']; // Will update an existing company record
$project->save();

Attributes of the related model will be massively assigned using the `load() method. So remember to declare the according attributes as safe in the rules of the related model.

Note: Only newly created or changed related models will be saved. See the PHPUnit tests for more examples.

Populate additional junction table columns in a many-to-many relation

In a many-to-many relation involving a junction table additional column values can be saved to the junction table for each model. See the configuration section for examples.

Note: If junction table properties are configured for a relation, the rows associated with the related models in the junction table will be deleted and inserted again on each saving to ensure that changes to the junction table properties are saved too.

Validation

Every declared related models will be validated prior to be saved. If any validation fails, for each related model attribute in error, an error associated with the named relation will be added to the owner model.

For hasMany() relations, the index of the related model will be used to identify the associated error message.

It is possible to specify the validation scenario for each relation by declaring an associative array in which the scenario key must contain the needed scenario value. For instance, in the following configuration, the links related records will be validated using the Link::SOME_SCENARIO scenario:

...
    public function behaviors()
    {
        return [
            'saveRelations' => [
                'class'     => SaveRelationsBehavior::className(),
                'relations' => ['company', 'users', 'links' => ['scenario' => Link::SOME_SCENARIO]]
            ],
        ];
    }
...

It is also possible to set a relation scenario at runtime using the setRelationScenario as follow:

$model->setRelationScenario('relationName', 'scenarioName');

Tips: For relations not involving a junction table by using the via() or viaTable() methods, you should remove the attributes pointing to the owner model from the 'required' validation rules to be able to pass the validations.

Note: If an error occurs for any reason during the saving process of related records in the afterSave event, a yii\db\Exception will be thrown on the first occurring error. An error message will be attached to the relation attribute of the owner model. In order to be able to handle these cases in a user-friendly way, one will have to catch yii\db\Exception exceptions.

Delete related records when the main model is deleted

For DBMs with no built in relational constraints, as of 1.5.0 release, one can now specify a relation to be deleted along with the main model.

To do so, the relation should be declared with a property cascadeDelete set to true. For example, related projectLinks records will automatically be deleted when the main model will be deleted:

...
'saveRelations' => [
    'class'     => SaveRelationsBehavior::className(),
    'relations' => [
        'projectLinks' => ['cascadeDelete' => true]
    ],
],
...

Note:. Every records related to the main model as they are defined in their ActiveQuery statement will be deleted.

Populate the model and its relations with input data

This behavior adds a convenient method to load relations models attributes in the same way that the load() method does. Simply call the loadRelations() with the according input data.

For instance:

$project = Project::findOne(1);
/**
 * $_POST could be something like:
 * [
 *     'Company'     => [
 *         'name' => 'YiiSoft'
 *     ],
 *     'ProjectLink' => [
 *         [
 *             'language' => 'en',
 *             'name'     => 'yii',
 *             'link'     => 'http://www.yiiframework.com'
 *         ],
 *         [
 *             'language' => 'fr',
 *             'name'     => 'yii',
 *             'link'     => 'http://www.yiiframework.fr'
 *         ]
 *     ]
 * ];
 */
$project->loadRelations(Yii::$app->request->post());

You can even further simplify the process by adding the SaveRelationsTrait to your model. In that case, a call to the load() method will also automatically trigger a call to the loadRelations() method by using the same data, so you basically won't have to change your controllers.

The relationKeyName property can be used to decide how the relations data will be retrieved from the data parameter.

Possible constants values are:

  • SaveRelationsBehavior::RELATION_KEY_FORM_NAME (default): the key name will be computed using the model formName() method
  • SaveRelationsBehavior::RELATION_KEY_RELATION_NAME: the relation name as defined in the behavior declarations will be used

Get old relations values

To retrieve relations value prior to there most recent modification until the model is saved, the following methods can be used:

  • getOldRelation($name): Get a named relation old value.
  • getOldRelations(): Get an array of relations index by there name with there old values.

Notes:

  • If a relation has not been modified yet, its initial value will be returned
  • Only relations defined in the behavior parameters will be returned

Get dirty relations

To deal with dirty (modified) relations since the model was loaded, the following methods can be used:

  • getDirtyRelations(): Get the relations that have been modified since they are loaded (name-value pairs)
  • markRelationDirty($name): Mark a relation as dirty even if it's not been modified.

yii2-save-relations-behavior's People

Contributors

bookin avatar dd174 avatar execut avatar juban avatar k0r73z avatar leandrogehlen avatar malinink avatar sankam-nikolya avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

yii2-save-relations-behavior's Issues

Enh: Аdjust access to save the relation

Is there such a functional?
For example, I need adjust access. So that only the admin could save relation tags

public function scenarios()
{
    $s = parent::scenarios();
    if(Yii::$app->user->can('admin')){
        $s[self::SCENARIO_DEFAULT][] = 'tags';
    }
    return $s;
}
var_dump($project->isAttributeSafe('tags')); //false

But relation tags save anyway when $project->save();

Load relations with Json Data

This JSON can't be loaded on $model->load(Yii::$app->request->post())... Just with : $model->attributes, but this doesn't load the relations
{ "id":"6", "idEscolaridade":"16", "descricao":"teste", "salario":5500.00, "Escolaridades":{ "id":"16", "descricao":"testando" } }

I'm using 'jaacoder/yii2-activated' to mapping the attributes, works for the parent model, but doesn't work for related models (ID column only)

Using behavior for relations

Hi!

You can make the functionality, that can configure relations through behavior?

Before:

    public function getUser()
    {
        return $this->hasOne(User::class, ['user_id' => 'id']);
    } 

After:

...
    private User $user
...
    /**
     * @var User $user
     **/
    public function getUser()
    {
        return $this->user;
    }

...
    public function behaviors()
    {
        return [
            'saveRelations' => [
                'class'     => SaveRelationsBehavior::class,
                'relations' => [
                     'users' => [
                          'class' => User::class,
                          'relation' => 'has-one',
                          'link' => ['user_id' => 'id']
                     ],
                ],
            ],
        ];
    }  
...

Now we can call get User and we won't get AR in response.

Some issue with one-to-one relationship

Thank you for your useful extension. I would like to give you feedback to enhance your extension. Version is 1.3.1.

I have some issue with saving new models Dummy and DummyBrother if I try to save with validation. There will be an SQL error anyway. Saving Dummy and DummyMany models works fine.

2017-10-29 22:07:15 [127.0.0.1][-][-][warning][lhs\Yii2SaveRelationsBehavior\SaveRelationsBehavior::saveRelatedRecords] yii\db\Exception was thrown while saving related records during beforeValidate event: SQLSTATE[HY000]: General error: 1364 Field 'dummy_id' doesn't have a default value

I have to save Dummy model without validation. It works if I do only like that.

I would be grateful if this case would be considered in tests.

...
class Dummy extends \yii\db\ActiveRecord
{
    public function behaviors()
    {
        return [
            'saveRelations' => [
                'class'    => SaveRelationsBehavior::class,
                'relations' => ['dummyBrother', 'dummyManies']
            ],
        ];
    }

    public function transactions()
    {
        return [
            self::SCENARIO_DEFAULT => self::OP_ALL,
        ];
    }
...
CREATE TABLE dummy (
  id int(11) NOT NULL AUTO_INCREMENT,
  name varchar(50) NOT NULL,
  PRIMARY KEY (id)
)
ENGINE = INNODB
AUTO_INCREMENT = 1
CHARACTER SET utf8
COLLATE utf8_general_ci
ROW_FORMAT = DYNAMIC;

CREATE TABLE dummy_brother (
  dummy_id int(11) NOT NULL,
  name varchar(50) NOT NULL,
  PRIMARY KEY (dummy_id),
  UNIQUE INDEX UK_dummy_brother_dummy_id (dummy_id),
  CONSTRAINT FK_dummy_brother_dummy_id FOREIGN KEY (dummy_id)
  REFERENCES dummy (id) ON DELETE NO ACTION ON UPDATE RESTRICT
)
ENGINE = INNODB
CHARACTER SET utf8
COLLATE utf8_general_ci
ROW_FORMAT = DYNAMIC;

CREATE TABLE dummy_many (
  id int(11) NOT NULL AUTO_INCREMENT,
  dummy_id int(11) NOT NULL,
  name varchar(50) NOT NULL,
  PRIMARY KEY (id),
  CONSTRAINT FK_dummy_many_dummy_id FOREIGN KEY (dummy_id)
  REFERENCES dummy (id) ON DELETE NO ACTION ON UPDATE RESTRICT
)
ENGINE = INNODB
AUTO_INCREMENT = 1
CHARACTER SET utf8
COLLATE utf8_general_ci
ROW_FORMAT = DYNAMIC;

hasOne relation not rollback when owner model is not valid and relation are inserted

The hasOne relationships are saved and inserted in beforeValidate and if owner model not inserted due to validation error hasOne inserted relations not rollback.

How to Test:

In model Project, name attribute is required and if you don't set name, $project->save() will be failed, but company record willl be inserted in database.

public function testSaveNewHasOneRelationShouldSucceed()
{
$project = new Project();
//$project->name = "Java";
$company = new Company();
$company->name = "Oracle";
$project->company = $company;
$this->assertTrue($company->isNewRecord, 'Company should be a new record');
$this->assertTrue($project->save(), 'Project could not be saved');
$this->assertNotNull($project->company_id, 'Company ID should be set');
$this->assertEquals($project->company_id, $company->id, 'Company ID is not the one expected');
}

Fail when parent model has more than one PRIMARY KEY

Hello,

In short words:
If my model has more than one PK like 'id' and 'timestamp'.
Then your code is failing here:


    private function _prepareHasOneRelation(BaseActiveRecord $model, $relationName, ModelEvent $event)
    {
        Yii::debug("_prepareHasOneRelation for {$relationName}", __METHOD__);
        $relationModel = $model->{$relationName};
        $this->validateRelationModel(self::prettyRelationName($relationName), $relationName, $model->{$relationName});
        $relation = $model->getRelation($relationName);
        $p1 = $model->isPrimaryKey(array_keys($relation->link)); <<<<HERE only 'id' is passed to isPrimaryKey()
        $p2 = $relationModel::isPrimaryKey(array_values($relation->link));

        if ($relationModel->getIsNewRecord() && $p1 && !$p2) {
            // Save Has one relation new record
            if ($event->isValid && (count($model->dirtyAttributes) || $model->{$relationName}->isNewRecord)) {
                Yii::debug('Saving ' . self::prettyRelationName($relationName) . ' relation model', __METHOD__);
                if ($model->{$relationName}->save()) {
                    $this->_savedHasOneModels[] = $model->{$relationName};
                }
            }
        }
    }

But frankly speaking I'm not sure that bug on your side.
Thats why I've rised same question on YII2 project
And where I described my problem in all details.

If I where you, I would avoid usage of function BaseActiveRecord::isPrimaryKey()
And use your own implementaion instead.

Error Unable to link models: the primary key of

You have an error from 384 to 386 lines.

$p1 = $model->isPrimaryKey(array_keys ($relation->link));
$p2 = $relationModel::isPrimaryKey(array_values($relation->link));
if ($relationModel->getIsNewRecord () & & $p1 && !$p2) {
...

What's the point? With such a relations

    public function getCompany()
    {
        return $this->hasOne(Company:: className (), ['id' = > 'company_id']);
    } 

if the parent class has properties primarykey (in your example on the main page Project ) the same as the relation class (in your example on the main page Company ) then your behavior will work.
But if in Project establish primaryKey as project_id the will error

Unable to link models: the primary key of ...

because relation class not save
as for me it is more correct to write the following code

$p1 = $model->isPrimaryKey(array_values($relation->link));
$p2 = $relationModel:: isPrimaryKey(array_keys($relation->link));
if ($relationModel->getIsNewRecord() && !$p1 & & $p2) {

почему так для relation
$this->hasOne(Company::className(), ['id' => 'company_id']);


array_keys($relation->link)='id'
array_values($relation->link)='company_id'

Sort related models

It will be great if related models will be sorted and user can swap the related models

updated values of the relations cannot be correctly accessed in the afterSave method of the master model

Hello!
I don't know exactly is this a bug or not but will try explain my problem.
Suppose we have Orders and OrderItems.

Orders:

class Order ... {

....

public function behaviors()
{
return [
.....
[
'class' => SaveRelationsBehavior::class,
'relations' => [
'orderItems'
]
],
.....
];
}

public function getOrderItems()
{
return $this->hasMany(OrderItem::class, ['order_id' => 'order_id']);
}

public function afterSave($insert, $changedAttributes)
{
var_dump(count($this->orderItems)); die();
retrun parent::afterSave($insert, $changedAttributes);
}

....

}

$order = new Order();
$order->orderItems = OrderItem[]; // simplified code
$order->save();

after that i see that afterSave method: count($this->orderItems) = 0.

but

$order = Order::findOne(1);
$order->orderItems = OrderItem[]; // simplified code
$order->save();

after that i see that afterSave: count($this->orderItems) > 0.

When i insert a new Order, before insert OrderItems i want to modify them. I can modify only when updating the Order, not when inserting.

Nested model changes does not save if parent model not dirty.

I have three level of models:

model1 {
  id: id1,
  prop1: value1,
  nested1: [
    model2 {
      id: id2,
      prop2: value2,
      nested2: [
        model3 {
          id: id3,
          prop3: value3
        }
      ]
    }
  ]
}

I want to change only prop3 of model3, but can not do this, because parent model has no dirty attributes, so nested models does not saves:

564      if (count($relationModel->dirtyAttributes)) {
565         if ($relationModel->validate()) {
566             $relationModel->save();
567         } else {
568             $this->_addError($relationModel, $owner, $relationName, self::prettyRelationName($relationName));
569             throw new DbException('Related record ' . self::prettyRelationName($relationName) . ' could not be saved.');
570         }
571      }

https://github.com/la-haute-societe/yii2-save-relations-behavior/blob/master/src/SaveRelationsBehavior.php#L564

feature request - work only existing models, or create new

Hi,

i have a scenario where i would like to work on huge form saved by this behavior. My only problem is i need to put the ids on the form for multi input if i would like to keep old records, and not always delete and create new ones.

what if somebody is overwriting id-s in hidden input than this behaviour will try to load it from db with that id. It could happen the user receives data which they should not see, and will linked to him.

i would suggest a small extra here, there could be a mode where it can only work with existing models, it checks was the model already in hasmany relation, if not then it will be created.

what do you think?

How Save hasOne relation from InputForm

I have

class Project extends \yii\db\ActiveRecord
{
    use SaveRelationsTrait; // Optional

    public function behaviors()
    {
        return [
            'saveRelations' => [
                'class'     => SaveRelationsBehavior::className(),
                'relations' => ['videos', 'company']
            ],
        ];
    }

    /**
     * @return ActiveQuery
     */
    public function getCompany()
    {
        return $this->hasOne(Company::className(), ['id' => 'company_id']);
    }

    /**
     * @return ActiveQuery
     */
    public function getVideos()
    {
        return $this->hasMany(Videos::className(), ['project_id' => 'id']);
    }
}

and form

$form->field($project, 'company[name]')->textarea(); // value is it, but not saved
$form->field($project, 'videos[0][name]')->textarea(); // saved good!
$form->field($project, 'videos[1][name]')->textarea(); // saved good!

In ProjectController I see

$_POST[Project][company][name] = 'Company name';  // This value not saved
$_POST[Project][videos][0][name] = 'Video 1 name'; // // This value saved

    public function actionUpdate($id)
    {
        $model = Project::findOne($id);
        $request = Yii::$app->request;
        $load = $model->load($request->post());

        $model->save(); // relation video saved, relation company - not saved

}

Company name not saved in my controller.

hasMany relations with no pivot table are not saved correctly

For hasMany relations not using a junction table with via() method, records are not saved during the beforeValidate event if the foreign key is set as 'NOT NULL'.
These kind of record should be saved during the afterSave event of the owner model.

1:1 connection - feature request

Hi,

do you see a solution where it could work with 1:1 connections as well?

i have a table: product (primary key product_id)
and product_coupon (primary key product_id)
and product_room (primary key product_id)

product_coupon, product_room are extending product base info.

what do you think?

attributeBehavior for primarykey reset populated hasmany relations

i have a UniquIdBehavior for id (primary key)

this reset populated relations, after yiisoft/yii2#13618 pr merged:

public function __set($name, $value)
    {
        if ($this->hasAttribute($name)) {
            if (
                !empty($this->_relationsDependencies[$name])
                && (!array_key_exists($name, $this->_attributes) || $this->_attributes[$name] !== $value)
            ) {
                $this->resetDependentRelations($name);
            }
            $this->_attributes[$name] = $value;
        } else {
            parent::__set($name, $value);
        }
    }

Trying to set ID of parent model to null

Hello!

https://monosnap.com/file/BV4PyFdUKuOLvhoj6K3a06wNKcqpPS

I have a problem with saving parent model. When I specifying relation on line SaveRelationBehavior:253 behavior replace primary key of main model and set foreign key of related model.
foreach ($relation->link as $relatedAttribute => $modelAttribute) { if ($model->{$modelAttribute} !== $model->{$relationName}->{$relatedAttribute}) { $model->{$modelAttribute} = $model->{$relationName}->{$relatedAttribute}; } }

but I have related attribute value is NULL and it's breaking saving process;

I think, solution is check NOT EMPTY related attribute ($model->{$relationName}->{$relatedAttribute})

Only write, not rewrite

I have a relation (has many) model and when saved common model I would like SaveRelationsBehavior only write new record to db. Now SaveRelationsBehavior every time rewriting relations data. This my behavior in model:

public function behaviors() {
        return [
            [
                'class' => SaveRelationsBehavior::class,
                'relations' => [
                    'vacancies',
                    'resumes'
                ],
            ],
        ];
    }

So, how can I do to only writer SaveRelationsBehavior?

Allow saving extra columns to junction table

Currently it the behavior code it is hard coded that the yii\db\BaseActiveRecord->link method is called without a possible third parameter.

The third parameter is used for passing additional columns data to be saved to the junction table in a many-to-many relationship through a junction table.
http://www.yiiframework.com/doc-2.0/yii-db-baseactiverecord.html#link()-detail

It would be nice if the behavior would allow to pass and save/update such additional columns in the junction table utilizing this native Yii2 method.

For example it could be made with declaring a callback in the behavior configuration that returns an array of the junction table column values for each ActiveRecord instance being saved,

An example:

/** @inheritdoc */
    public function behaviors()
    {
        return [
            'saveRelations' => [
                'class'     => SaveRelationsBehavior::class,
                'relations' => [
                    'photos'
                ],
                'junctionTableProperties' => [
                    'photos' => function (\yii\db\ActiveRecord $record) :array {
                        return [
                            'order' => $record->order
                        ];
                    }
                ]
            ]
        ];
    }

Separation of validation and saving does unique validation not possible

When i save a model with "hasMany" relation it does validate each of relations from an array and then saving them with ->save(false). But i have the unique validation rule in relation model, which shouldn't let it save both of them in this example:

ParentModel [
    'property' => 'value',
    'relation' => [
        [
            'name' => 'ex1',
            'uniqueField' => 1,
        ],
        [
            'name' => 'ex2',
            'uniqueField' => 1,//shouldn't be saved
        ],
    ],
]

Logs:

  1. SELECT CASE WHEN EXISTS(SELECT * FROM relation WHERE uniqueField = 1) THEN 1 ELSE 0 END FROM DUAL; /* validation of ex1 */
  2. SELECT CASE WHEN EXISTS(SELECT * FROM relation WHERE uniqueField = 1) THEN 1 ELSE 0 END FROM DUAL; /* validation of ex2 */
  3. INSERT ... ex1
  4. INSERT ... ex2

Instead of SELECT /ex1/, INSERT /ex1/, SELECT /ex2/, addError

Delete relation on array field postgresql

I have array field with ids of 3 related items and hasMany relation. Then I delete one of them and
when I try to save the model it is deleted. I think the problem is in the method $model->unlink($relationName, $initialModels[$key], true). Since there is no via table and a parameter $delete is true it deletes the parent model.

Strange behavior saving newly created hasOne entity

I have a Profile Model. The model saved into database and has an ID. It has relation
public function getPerson()
{
return $this->hasOne(Person::className(), ['user_id' => 'user_id']);
}
Both user_id are primary key (One to One). But no person record exists.
Of course a have no

field($model, 'person[user_id]')->hiddenInput(); ?>

line in the view file

Then i save the Profile. It throws an constrait error.

if ($model->{$modelAttribute} !== $model->{$relationName}->{$relatedAttribute}) {
sets primary key of Profile model to null because $model->{$modelAttribute} !== $model->{$relationName}->{$relatedAttribute} where $model->{$relationName}->{$relatedAttribute} is null

It happens because Person model has not been saved because

if ($relationModel->getIsNewRecord() && $p1 && !$p2) {
Line return false because $p2 is primary key. And person model has been validated only. Why are so difficult conditions (i am not allowed to have both as pk)?

#29 has a similar issue. Possibly we don't unserstand how to manipulate hasOne relations..

Yii::debug("Setting foreign keys for {$relationName}", __METHOD__);
writes me "Setting foreign keys for person" but
$model->{$modelAttribute} = $model->{$relationName}->{$relatedAttribute};
sets Profile property to null in my case.

Updating multiple records in nested related model

Scenario :

$scenarios[self::SCENARIO_SAVE_STEP_2] = ['company', 'address', 'vehicles', 'id'];

Behavior :

    public function behaviors() {
            return [
                'saveRelations' => [
                    'class'     => SaveRelationsBehavior::className(),
                    'relations' => [
                        'company' => ['scenario' => Company::SCENARIO_COMPANY],
                        'address' => ['scenario' => Address::SCENARIO_PROFILE_COMPANY],
                        'vehicles' => ['scenario' => Vehicle::SCENARIO_PROFILE_VEHICLE]
                      ]
                  ],
            ];
    }

example : In User Model,

    $user = User::findOne($this->id);
    $user->scenario = User::SCENARIO_SAVE_STEP_2;
    $user->load(Yii::$app->getRequest()->getBodyParams(), '');

Payloads,

    {
    id:1,
    name : "adam",
        company : {
            id : "2",
            userid : "1",
            name : "My Company",
            logo : {
                id : "1",
                url : "http://myimage.com/logo.png"
                }
        },
        address : {
            id : "1",
            userid : "1",
            name : "My Company",
        },
        vehicles : [
            { 

             id : 2,
             type : "two wheeler"
             licenseImages : [{
                id : "3",
                url : "http://myimage.com/license.png"
                }]

             },

            { 

             id : 3,
             type : "four wheeler"
             licenseImages : [{
                id : "4",
                url : "http://myimage.com/license.png"
                }]

             }

        ]
    }

This is working:

1. New record save for hasOne relation, when primary key is given blank.

2. New data for Nested relation hasMany, data is saving.

This is NOT working:

1. Existing record update for hasOne relation, when primary key is given.
2. Existing data for Nested relation hasMany, data is not saving, with primary key given in payload.
3. New data is not saving for hasMany relation when data is given in array (ref. vehicles in above example).
4. Existing data is not saving for hasMany relation when data is given in array (ref. vehicles in above example), with primary key i.e id.
5. New or Existing data for Nested relation hasMany, data is not saving, with primary key given in payload. (ref. vehicles->licenseImages in above example)

My system configuration :
Yii2 = Version 2.0.14
PHP = Version 7.1.20
Mysql Server = Version 5.7.23
Extenstion = Version ^1.5

Can't set virtual attribute for ActiveRecord

My ActiveRecord class has the attribute

class Profile extends ActiveRecord
{
   /**
     * @var bool
     */
    public $agree;

public function rules()
    {
        return [
         
            [['agree'], 'required', 'on' => 'insert'],
         
        ];
    }
}

But i have got an error "Failed to set unsafe attribute 'agree' in 'app\models\user\Profile'."

I found out we have to set scenario here if scenario provided by behavior

$relationModel->setAttributes($data);

Anyway it helps me to work/ Isn't it the right case?

Validation trouble

if i use input form like

<?= $form->field($user, 'profile[opf_id]')->dropDownList(Opf::getDropDown(), ['prompt' => ''])
                        ->label(Profile::instance()->getAttributeLabel('opf_id')); ?>

i didn't watch validation errors.

i found out that your method has to be like

 private function _addError($relationModel, $owner, $relationName, $prettyRelationName)
    {
        foreach ($relationModel->errors as $attribute=>$attributeErrors) {
            foreach ($attributeErrors as $error) {
                $owner->addError("{$relationName}[{$attribute}][]", "{$prettyRelationName}: {$error}");
            }
        }
    }

it helps to render any error for related inputs.
How is the right way to build form-field statement and validate input data in my case?

Transaction object may not be defined

The transaction object may not be defined if an exception occurs in the _saveRelatedRecords method and no transactions are declared in the owner model.

No SaveRelationsTrait.php file

There is no SaveRelationsTrait.php file, when I install this extension throw composer.
"src" folder contains only SaveRelationsBehavior.php

Don't work with some relations

Migration

 $this->createTable('{{%project}}', [
            'id' => $this->primaryKey(),
            'name' => $this->string(32)->notNull(),
        ]);
 $this->createTable('{{%project_company}}', [
            'id' => $this->primaryKey(),
            'project_id' => $this->integer()->notNull(),
            'title' => $this->string(64)->notNull(),
            'desc' => $this->text()->notNull(),
        ]);

Model

use lhs\Yii2SaveRelationsBehavior\SaveRelationsBehavior;

class Project extends \yii\db\ActiveRecord
{
    public function behaviors()
    {
        return [
            'saveRelations' => [
                'class'     => SaveRelationsBehavior::className(),
                'relations' => [ 'company'],
            ],
        ];
    }

    public function transactions()
    {
        return [
            self::SCENARIO_DEFAULT => self::OP_ALL,
        ];
    }

    /**
     * @return ActiveQuery
     */
    public function getCompany()
    {
        return $this->hasOne(ProjectCompany::className(), ['project_id' => 'id']);
    }

   // other relations
}

This code don't work, because project_id don't set automatically

$project = Project::findOne(1);
$project->company = ['title' => 'new company']; 
$project->save();

usage question maybe a bug?

hi,

i just cannot find out how this is working. i did put it in my project, did set up as an document, but it is just not working. i did recognised some parts which could be buggy, but of course if it works for everyone else, than the problem will be with me.

if i add new line here :

protected function _setRelationForeignKeys($relationName)
{
/** @var BaseActiveRecord $owner /
$owner = $this->owner;
/
* @var ActiveQuery $relation */
$relation = $owner->getRelation($relationName);
if ($relation->multiple === false && !empty($owner->{$relationName})) {
Yii::debug("Setting foreign keys for {$relationName}", METHOD);
foreach ($relation->link as $relatedAttribute => $modelAttribute) {
if ($owner->{$modelAttribute} !== $owner->{$relationName}->{$relatedAttribute}) {
if ($owner->{$relationName}->isNewRecord) {
$owner->{$relationName}->{$relatedAttribute} = $owner->{$modelAttribute};
$owner->{$relationName}->save();
}
$owner->{$modelAttribute} = $owner->{$relationName}->{$relatedAttribute};
}
}
}
}

than it starts working for me as well. i just could not find out where will be set all the foreign keys.

the another one is:
_prepareHasOneRelation
in my mind this one is wrong:
$p1 = $model->isPrimaryKey(array_keys($relation->link));
$p2 = $relationModel::isPrimaryKey(array_values($relation->link));
the first one should be array_values, second array_keys. but maybe i don't understand the porpuse of the method.

my setup is really strait forward:

        'saveRelations' => [
            'class' => SaveRelationsBehavior::class,
            'relations' => [
                'personDetail',
                'personAddress',
                'personEmails',
                'personPhones',
                'personSocials',
                'user',
            ],
        ]

but it cannot handle new records, it deals perfect with existing ones. My db should be ok, gii can generate model properly.

"private" replace with "protected".

Now some properties and methods are declared as "private". It does not allow to expand the behavior.

I suggest instead of "private" to use "protected".

Deep nesting relations saving

Wrong saving a nested relations with one main.

Models relations example.

Main has many Child, Child has one SubChild.

<?php
namespace api\models;

use lhs\Yii2SaveRelationsBehavior\SaveRelationsBehavior;
.....
class Main extends \yii\db\ActiveRecord
{

    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return [
			.....
            [
                'class' => SaveRelationsBehavior::className(),
                'relations' => [
                    'childs'
                ]
            ],
			.....
        ];
    }

    /**
     * @return array
     */
    public function transactions()
    {
        return [
            self::SCENARIO_DEFAULT => [
                self::OP_ALL
            ]
        ];
    }

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return '{{%main}}';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        .....
    }
	
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getChilds()
    {
        return $this->hasMany(Child::className(), ['child_id' => 'id']);
    }

    /**
     * @inheritdoc
     * @return \api\models\scopes\MainQuery the active query used by this AR class.
     */
    public static function find()
    {
        return new \api\models\scopes\MainQuery(get_called_class());
    }
}
<?php

namespace api\models;
.....
use lhs\Yii2SaveRelationsBehavior\SaveRelationsBehavior;

class Child extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public function behaviors()
    {
        return [
			......
            [
                'class' => SaveRelationsBehavior::className(),
                'relations' => [
                    'subChild'
                ]
            ],
			.....
        ];
    }

    /**
     * @return array
     */
    public function transactions()
    {
        return [
            self::SCENARIO_DEFAULT => [
                self::OP_ALL
            ]
        ];
    }

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return '{{%child}}';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
			....
        ];
    }

    public static function create(/** some params **/)
    {
        $iteration = new static();
        $iteration->subChild = SubChild::create(/** some params **/);

        return $child;
    }

    /**
     * @return \yii\db\ActiveQuery
     */
    public function getSubChild()
    {
        return $this->hasOne(SubChild::className(), ['sub_child_id' => 'id']);
    }

 
    public static function find()
    {
        return new \api\models\scopes\ChildQuery(get_called_class());
    }
}
<?php

namespace api\models;

class SubChild extends \yii\db\ActiveRecord
{

    /**
     * @return array
     */
    public function transactions()
    {
        return [
            self::SCENARIO_DEFAULT => [
                self::OP_ALL
            ]
        ];
    }

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return '{{%sub_child}}';
    }

    /**
     * @inheritdoc
     */
    public function rules()
    {
        return [
			....
        ];
    }

    /**
     * @return static
     */
    public static function create(/**  some params **/)
    {
        $subChild = new static();

        return $subChild;
    }
}

Save Main model instance

$banner = new Main();
$banner->load($form->attributes,'');
$banner->child = Child::create(/** some params**/);

$banner->save();

Trouble

In this case relations must saves recursively, but in beforeValidate behavior method raises exception:

[lhs\Yii2SaveRelationsBehavior\SaveRelationsBehavior::saveRelatedRecords] yii\db\IntegrityException was thrown while saving related records during beforeValidate event: SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`schema`.`sub_child`, CONSTRAINT `fk-sub_child-child` FOREIGN KEY (`child_id`) REFERENCES `child` (`id`) ON UPDATE CASCADE)
The SQL being executed was: INSERT INTO `sub_child` (`state`, `created_at`) VALUES ('skipped', UNIX_TIMESTAMP())
    in /dd_main/web/sites/chulakov/bander/api/vendor/la-haute-societe/yii2-save-relations-behavior/src/SaveRelationsBehavior.php:300
    in /dd_main/web/sites/chulakov/bander/api/vendor/la-haute-societe/yii2-save-relations-behavior/src/SaveRelationsBehavior.php:233
    in /dd_main/web/sites/chulakov/bander/api/vendor/la-haute-societe/yii2-save-relations-behavior/src/SaveRelationsBehavior.php:340
2017-11-06 12:33:16 [192.168.1.1][1][-][error][yii\db\Exception] exception 'yii\db\Exception' with message 'Failed to commit transaction: transaction was inactive.' in /dd_main/web/sites/chulakov/bander/api/vendor/yiisoft/yii2/db/Transaction.php:151
Stack trace:
#0 /dd_main/web/sites/chulakov/bander/api/vendor/yiisoft/yii2/db/ActiveRecord.php(486): yii\db\Transaction->commit()
#1 /dd_main/web/sites/chulakov/bander/api/vendor/yiisoft/yii2/db/BaseActiveRecord.php(646): yii\db\ActiveRecord->insert(true, NULL)
#2 /dd_main/web/sites/chulakov/bander/api/api/repositories/BannerRepository.php(17): yii\db\BaseActiveRecord->save()
#3 /dd_main/web/sites/chulakov/bander/api/api/services/BannerService.php(64): api\repositories\BannerRepository->save(Object(api\models\Banner))
#4 /dd_main/web/sites/chulakov/bander/api/api/controllers/BannerController.php(95): api\services\BannerService->upload(Object(api\forms\BanderUpload))
#5 [internal function]: api\controllers\BannerController->actionUpload()
#6 /dd_main/web/sites/chulakov/bander/api/vendor/yiisoft/yii2/base/InlineAction.php(57): call_user_func_array(Array, Array)
#7 /dd_main/web/sites/chulakov/bander/api/vendor/yiisoft/yii2/base/Controller.php(157): yii\base\InlineAction->runWithParams(Array)
#8 /dd_main/web/sites/chulakov/bander/api/vendor/yiisoft/yii2/base/Module.php(528): yii\base\Controller->runAction('upload', Array)
#9 /dd_main/web/sites/chulakov/bander/api/vendor/yiisoft/yii2/web/Application.php(103): yii\base\Module->runAction('banner/upload', Array)
#10 /dd_main/web/sites/chulakov/bander/api/vendor/yiisoft/yii2/base/Application.php(386): yii\web\Application->handleRequest(Object(yii\web\Request))
#11 /dd_main/web/sites/chulakov/bander/api/api/web-test/index.php(18): yii\base\Application->run()
#12 {main}

Foreign key value not append to insert query. This error appears in saving with validation. If save the main instance without validation no errors raised.

hasOne relation via() is not working as expected

it creates new linking all the time, if i am populating it from a POST.

becouse of this: (line 279)
$relationModel = null; if (!empty($fks)) { $relationModel = $modelClass::findOne($fks); }

it creates a new instance
than (line 664)
if ($this->_oldRelationValue[$relationName] !== $owner->{$relationName}) {
will be false all the time. it links again, whenever it was a existing relation or not.

i am using php7.1

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.