Данное расширение используется для того, что бы при сохранении данных сущности (модели), например User:
$user->save()

происходило автоматическое сохранение данных во все связанные таблицы (вместо того, чтобы делать их сохранение вручную). Такой функционал доступен в Laravel-5 «из коробки», а в yii2, для этого, можно использовать специальное расширение: la-haute-societe/yii2-save-relations-behavior.


Особенности расширения:
  • поддерживаются отношения hasMany() и hasOne();
  • работает с существующими связанными объектами так и позволяет создавать новые;
  • поддерживаются составные первичные ключи;
  • используется только чистый Active Record API, поэтому работает с любым драйвером DB.

Установка:
composer require --prefer-dist la-haute-societe/yii2-save-relations-behavior



Пример.
Например есть таблицы:
- users;
- user_networks;
Таблица users содержит данные пользователей, а таблица user_networks содержит данные соц.сетей к которым привязан каждый пользователь (название соц.сети и уникальный идентификатор данного пользователя). Т.е. связь один ко многим – один пользователь может быть зарегистрирован в нескольких соцсетях, при этом каждая строка таблицы user_networks может принадлежать только одному пользователю, т.к. она содержит его уникальный идентификатор.

Миграция для создания таблицы user_networks:
 public function up()
    {
        $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB';

        $this->createTable('{{%user_networks}}', [
            'id' => $this->primaryKey(),
            'user_id' => $this->integer()->notNull(),
            'identity' => $this->string()->notNull(),
            'network' => $this->string(16)->notNull(),
        ], $tableOptions);

        $this->createIndex('{{%idx-user_networks-identity-name}}', '{{%user_networks}}', ['identity', 'network'], true);

        $this->createIndex('{{%idx-user_networks-user_id}}', '{{%user_networks}}', 'user_id');

        $this->addForeignKey('{{%fk-user_networks-user_id}}', '{{%user_networks}}', 'user_id', '{{%users}}', 'id', 'CASCADE');
    }

    public function down()
    {
        $this->dropTable('{{%user_networks}}');
    }

Далее создаем сущность(модель) для данной таблицы - Network. Класс ее должен наследоваться от ActiveRecord:
<?php

namespace common\models;

/**
 * @property integer $id
 * @property integer $user_id
 * @property string $identity
 * @property string $network
 *
 * @property User $user
 */
class Network extends \yii\db\ActiveRecord
{
    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'user_networks';
    }

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

Если используется метод rules с правилами валидации, то нужно удалить проверку поля 'user_id' на обязательность заполнения, т.к. сохранением в это поле будет заниматься расширение.
Т.е. вместо
[['user_id', 'identity', 'network'], 'required'],
пишем
[['identity', 'network'], 'required'],

В основную сущность, в данном случае User, нужно добавить поведение для подключения класса расширения. Тогда данный класс будет следить за связанной таблицей (нужно указать в поведении) и сохранять в нее изменения при редактировании основной сущности.
Т.е. метод save нужно будет вызывать только для User, а данные в связанные с ним таблицы сохранятся автоматически.

В метод behaviors добавляем блок 'saveRelations' и добавляем метод transactions что бы при записи в связанные таблицы с помощью этого поведения использовались транзакции:
/**
 * @inheritdoc
 */
public function behaviors()
{
    return [
        TimestampBehavior::className(),
        'saveRelations' => [
            'class'     => SaveRelationsBehavior::className(),
            'relations' => [
                'networks',
            ],
        ],
    ];
}

//Использовать транзакции для SaveRelationsBehavior
public function transactions()
{
    return [
        self::SCENARIO_DEFAULT => self::OP_ALL,
    ];
}

/**
 * @return \yii\db\ActiveQuery
 */
public function getNetworks()
{
    return $this->hasMany(Network::className(), ['user_id' => 'id']);
}

Метод getNetworks связывает данную сущность с Network. По названию этого метода (с маленькой буквы и без префикса «get») в массив "relations" добавляем строку «networks». В массив 'relations' можно добавить несколько связанных сущностей и расширение за всеми ими будет следить и обновлять.


Использование.

  • При создании нового пользователя, сразу привязываем к нему таблицу с соцсетями (сущность Network). Для примера создаем новый объект Network. Данные сущности Network сохранятся при сохранении самой User, отдельно вызывать save не придется, кроме того, автоматически заполнится поле user_id.
$user = new User();
$user->username = 'admin';
$user->auth_key = Yii::$app->security->generateRandomString();
$user->password_hash = Yii::$app->security->generatePasswordHash('123456');
$user->email = 'admin@mail.ru';
$user->status = 10;

$network = new Network();
$network->identity = '3423434';
$network->network = 'vk';
$user->networks = $network;

$user->save();

  • Привязываем к User уже существующую сущность Network, по ее id (50):
$user = new User();
$user->username = 'admin2';
$user->auth_key = Yii::$app->security->generateRandomString();
$user->password_hash = Yii::$app->security->generatePasswordHash('123456');
$user->email = 'admin2@mail.ru';
$user->status = 10;

$user->networks = 50; //Network id=50

$user->save();

Можно привязать сразу несколько сущностей по id используя массив:
$user->networks = [50,78];
или получить нужный объект другим образом:
$user->networks = Network::findOne(['network' => 'vk']);


Связь многие ко многим через связывающую таблицу.

На примере таблиц:
  • users;
  • roles (поля: id, name);
  • user_role (поля: user_id, role_id)

В реальном проекте для таблицы roles создаем уникальный ключ для поля name, а для таблицы user_role создаем составной первичный ключ для обоих полей и ограничения внешних ключей для них.

В сущности User создаем метод для получения роли через связывающую таблицу таким образом:
public function getRoles()
{
    return $this->hasMany(Role::className(), ['id' => 'role_id'])->viaTable('user_role', ['user_id' => 'id']);
}
его и прописываем в поведении:
public function behaviors()
{
    return [
        TimestampBehavior::className(),
        'saveRelations' => [
            'class'     => SaveRelationsBehavior::className(),
            'relations' => [
                'roles',
            ],
        ],
    ];
}

Использование:
$user = new User();
$user->username = 'admin';
$user->auth_key = Yii::$app->security->generateRandomString();
$user->password_hash = Yii::$app->security->generatePasswordHash('123456');
$user->email = 'admin@mail.ru';
$user->status = 10;

$role = new Role();
$role->name = Role::ROLE_USER;
$user->roles = $role;

$user->save();

Т.е. так же как и раньше. В результате сохранения сущности User будут сохранены данные и в таблицу user_role и в roles. В данном примере создается новая роль. Если нужно привязать к пользователю уже существующую:
$role = Role::findOne(['name' => Role::ROLE_ADMIN]);
$user->roles = $role;

$user->save();

Заполнение доп. полей для связывающей таблицы.
Так же есть возможность сохранить что-то дополнительно в связывающую таблицу. Например связывающая таблица user_role кроме полей user_id и role_id содержит поле description, которое не участвует в связях и не заполняется автоматически. Заполнение поля description задается при настройке поведения:
public function behaviors()
{
    return [
        TimestampBehavior::className(),
        'saveRelations' => [
            'class'     => SaveRelationsBehavior::className(),
            'relations' => [
                'roles' => [
                    'extraColumns' => function($model){
                        /** @var $model Role */
                        return [
                            'description' => $model->name . '_desc'
                        ];
                    }
                ],
            ],
        ],
    ];
}

С помощью массива 'extraColumns' указываем, что нужно заполнить поля для связывающей таблицы. Указывается анонимная функция, которой в качестве аргумента передается модель таблицы с которой осуществляется связь (roles). Когда создается новый объект связанной таблицы или получаем уже существующие данные:
…
$role = Role::findOne(['name' => Role::ROLE_ADMIN]);
$user->roles = $role;
она заполняется этими данными и их можно использовать, если они нужны для связывающей таблицы. В данном случае в поле 'description' таблицы user_role сохранится значение «admin_desc».