
$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».