...

пятница, 13 июня 2014 г.

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, живете в Кишиневе (Молдова) или поблизости, присоединяйтесь к нам! Мы хотим собраться в оффлайне.

Подробности здесь!

Ждем всех!


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий