Yii2. Связи Active Record

в 20:25, , рубрики: active record, yii, yii2, метки: ,

Yii2. Связи Active Record 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js