СоХабр закрыт.

С 13.05.2019 изменения постов больше не отслеживаются, и новые посты не сохраняются.

H Yii2. Связи Active Record в черновиках Tutorial

Yii
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,
  ];
}

Это лишь некоторые методы. Остальные Вы найдёте в официальной документации.

Пример


Теперь я хочу показать как их можно использовать на примере.

Модель
class Post extends ActiveRecord
{
    // Будем использовать транзакции при указанных сценариях
    public function transactions()
    {
        return [
            self::SCENARIO_DEFAULT => self::OP_INSERT | 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;
             $tags[] = $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 loadUploadedImages()
    {
           $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();

        if ($post->load(Yii::$app->request->post())) {
            // Сохраняем загруженные файлы
            $post->loadUploadedImages();

            if ($post->save()) {
                return $this->redirect(['view', 'id' => $post->id]);
            }
        }
          
        return $this->render('create', [
            'post' => $post,
        ]);
     }
}


Вместо заключения


Если вы знаете что такое Yii Framework, живете в Кишиневе (Молдова) или поблизости, присоединяйтесь к нам! Мы хотим собраться в оффлайне.
Подробности здесь!

Ждем всех!

комментарии (6)

0
Ekstazi ,  
Спасибо, еще бы про контроль доступа в Yii2 рассказал.
0
codru ,  
Там в документации всё достаточно подробно описано, как мне показалось. По крайней мере с rbac разобраться проблем не составило. Ссылка
+3
AlexGx ,  

Использую Yii2 на продакшене. Полет отличный.

Хочу сделать небольшую ремарку по связям для тех кто будет переходить с Yii1. В Yii2 нет связи BELONGS_TO, вместо нее используют hasOne с обратной нотацией.

Пример. Было

...
 'option' => array(self::BELONGS_TO, 'Option', 'option_id'),
...


Стало
public function getOption()
{
    return $this->hasOne(Option::className(), ['id' => 'option_id']);
}
+1
AstRonin ,   * (был изменён)
опять мало… какие-то выдержки из форума…
на tutorial не тянет
+2
DIAgen ,   * (был изменён)
Не описали viaTable, Lazy и Eager Загрузку, inverseOf, joinWith и т.д, так это заметка об AR Relational Data.
0
ATLANT1S ,  

Сдаётся мне, на 8 строке ошибка. Заполнять надо бы $tags.

    public function setTagsString($value)
    {
        $tags = [];
      
        foreach (explode(',' $value) as $name) {
             $tag = new Tag();
             $tag->name = $name;
             $tag[] = $tag;
        }
       
        $this->setTags($tags);
    }