Yii2 еще только в бета-тестировани но видимо это никого не пугает. Многие начали использовать его даже в продакшене и под дулом пистолета, отказываются даже смотреть на код первой версии. Кто-то просто изучает новые возможности.
На форуме русского сообщества, все больше вопросов и обсуждения.
Оказалось что у многих возникли трудности с работой моделью ActiveRecord и связанными данными.
Я решил написать свой рецепт, возможно вы посчитаете его полезным, возможно поймете как делать не надо. В любом случае надеюсь материал будет полезен.
Что нам предлагает фреймворк для работы со связями?
Связи в новой версии фреймворка объявляются при помощи геттеров
public function getCategory()
{
return $this->hasOne(Category::className(), ['id' => 'category_id']);
}
Геттер возвращает ActiveQuery который можно дополнительно настроить перед загрузкой связанной модели.
$posts = Category::find($id)->getPosts()->limit(5)->order('created_at')->all();
Замечание:
Вы используете магию $post->category, вместо геттера, помните что так вы получаете результат запроса Query-объекта.
Другими словами $post->category === $post->getCategory()->one()
Методы работы со связями
populateRelation($relationName, $relatedModelOrArray) — добавляет связанную модель в родительскую.
Замечание:
Этот метод не проверяет объявлена ли связь между этими моделями (геттер), а так же не устанавливает нужные значения в атрибуты.
$post = new Post();
$post->populateRelation('category', new Category());
$post->populateRelation('tags', [new Tag(), new Tag()]);
link($relationName, relatedModel, $extraColumns = []) — в отличии от populateRelation этот метод кроме добавления связанной модели, также привязывает модели, расставляя нужные индексы и сразу же сохраняет ТОЛЬКО связанную модель. $extraColumns — сохранятся в pivot table, если связь осуществляется через нее.
$post = new Post();
$post->link('category', new Category());
$post->link('tags', new Tag());
$post->link('tags', new Tag());
Вам возможно захочется сохранять модели вместе со связями в одной транзакции. Для этого в Yii2 есть встроенные средства.
public function transactions()
{
return [
// scenario name => operation (insert, update or delete)
self::SCENARIO_DEFAULT => self::OP_INSERT | self::OP_UPDATE,
self::SCENARIO_UPDATE => self::OP_INSERT,
];
}
Это лишь некоторые методы, остальные Вы найдете в официальной документации.
Пример
Теперь я хочу показать как их можно использовать на примере
class Post extends ActiveRecord
{
// Будем использовать транзакции при указанных сценариях
public function transactions()
{
return [
self::SCENARIO_INSERT => self::OP_INSERT,
self::SCENARIO_UPDATE => self::OP_UPDATE,
];
}
public function getTags()
{
return $this->hasMany(Tag::className(), ['id' => 'tag_id'])
->viaTable('post_tag', ['post_id' => 'id']);
}
// Я предлагаю использовать сеттеры для связей,
// хотя это дополнительное телодвижение,
// но совсем не сложно писать сразу рядом с геттером.
// Зато очень удобно, т.к. сразу можно делать дополнительные
// изменения модели
public function setTags($tags)
{
$this->populateRelation('tags', $tags);
$this->tags_count = count($tags);
}
// Сеттер для получения тегов из строки, разделенных запятой
public function setTagsString($value)
{
$tags = [];
foreach (explode(',' $value) as $name) {
$tag = new Tag();
$tag->name = $name;
$tag[] = $tag;
}
$this->setTags($tags);
}
public function getCover()
{
return $this->hasOne(Image::className(), ['id' => 'cover_id']);
}
public function setCover($cover)
{
$this->populateRelation('cover', $cover);
}
public function getImages()
{
return $this->hasMany(Image::className(), ['post_id' => 'id']);
}
public function setImages($images)
{
$this->populateRelation('images', $images);
if (!$this->isRelationPopulated('cover') && !$this->getCover()->one()) {
$this->setCover(reset($images));
}
}
public function loadUploadedImage()
{
$images = [];
foreach (UploadedFile::getInstances(new Image(), 'image') as $file) {
$image = new Image();
$image->name = $file->name;
$images[] = $image;
}
$this->setImages($images);
}
public function beforeSave($insert)
{
if (!parent::beforeSave($insert)) {
return false;
}
// В beforeSave мы сохраняем связанные модели
// которые нужно сохранить до основной, т.е. нужны их ИД
// Не волнуйтесь о транзакции т.к. мы настроили,
// она будет начата при вызове метода `insert()` и `update()`
// Получаем все связанные модели, те что загружены или установлены
$relatedRecords = $this->getRelatedRecords();
if (isset($relatedRecords['cover'])) {
$this->link('cover', $relatedRecords['cover']);
}
return true;
}
public function afterSave($insert)
{
// В afterSave мы сохраняем связанные модели
// которые нужно сохранять после основной модели, т.к. нужен ее ИД
// Получаем все связанные модели, те что загружены или установлены
$relatedRecords = $this->getRelatedRecords();
if (isset($relatedRecords['tags'])) {
foreach ($relatedRecords['tags'] as $tag) {
$this->link('tags', $tag);
}
}
if (isset($relatedRecords['images'])) {
foreach ($relatedRecords['images'] as $image) {
$this->link('images', $image);
}
}
}
}
class PostController extends Controller
{
public function actionCreate()
{
$post = new Post();
// Устанавливаем нужный сценарий,
// например чтоб запустить транзакцию при сохранении
$post->setScenario(Post::SCENARIO_INSERT);
if ($post->load(Yii::$app->request->post())) {
// Сохраняем загруженные файлы
$this->loadUploadedImages();
if ($post->save()) {
return $this->redirect(['view', 'id' => $post->id]);
}
}
return $this->render('create', [
'post' => $post,
]);
}
}
Вместо заключения
Если вы знаете что такое Yii Framework, живете в Кишиневе (Молдова) или поблизости, присоединяйтесь к нам! Мы хотим собраться в оффлайне.
Подробности здесь!
Ждем всех!
Автор: slavcopost