bitver
@bitver

Есть ли возможность сэмулировать eager loading когда уже есть массив моделей?

Из документации:$authors = Author::find()->with('posts')->all(); вытащит все что нужно 2мя запросами
SELECT * FROM author;
SELECT * FROM post WHERE author_id IN (1, 3, 7, 10, ... X);


Что делать если у меня уже есть $authors = Author::find()->all(); //Без with() и хочется как-бы применить with() к массиву $authors?

P.S. сразу объясню, что пример сильно утрирован, на деле имеется массив моделей, сформированный сложной логикой на уровне php и я способен просто сформировать вручную запрос и заполнить массив данными, хочется все-же обойтись средствами фреймворка.
  • Вопрос задан
  • 175 просмотров
Решения вопроса 1
bitver
@bitver Автор вопроса
Не нашёл подобных методов во фреймворке, но удалось вытащить то, что нужно:
Класс который делает то, что описано в вопросе
<?php

namespace app\components;


use yii\base\Model;
use yii\db\ActiveQuery;
use yii\db\ActiveQueryInterface;
use yii\db\ActiveRecord;

class Eagerifier
{
    /**
     * @param $with
     * @param Model[] $models
     * @param bool $asArray
     * @throws \yii\base\InvalidConfigException
     */
    public static function populate($with, array &$models, $asArray = false)
    {
        $modelClass = '';
        foreach ($models as $key => $model) {
            $modelClass = $model->className();
            break;
        }

//        From ActiveQuery::findWith()
        $primaryModel = new $modelClass;
        $relations = self::normalizeRelations($primaryModel, $with);
        /* @var $relation ActiveQuery */
        foreach ($relations as $name => $relation) {
            if ($relation->asArray === null) {
                // inherit asArray from primary query
                $relation->asArray($asArray);
            }
            $relation->populateRelation($name, $models);
        }
    }

    /**
     * Adapted method from ActiveQuery
     *
     * @param ActiveRecord $model
     * @param array $with
     * @return ActiveQueryInterface[]
     */
    private static function normalizeRelations($model, $with)
    {
        $relations = [];
        foreach ($with as $name => $callback) {
            if (is_int($name)) {
                $name = $callback;
                $callback = null;
            }
            if (($pos = strpos($name, '.')) !== false) {
                // with sub-relations
                $childName = substr($name, $pos + 1);
                $name = substr($name, 0, $pos);
            } else {
                $childName = null;
            }

            if (!isset($relations[$name])) {
                $relation = $model->getRelation($name);
                $relation->primaryModel = null;
                $relations[$name] = $relation;
            } else {
                $relation = $relations[$name];
            }

            if (isset($childName)) {
                $relation->with[$childName] = $callback;
            } elseif ($callback !== null) {
                call_user_func($callback, $relation);
            }
        }

        return $relations;
    }
}


Использование: Eagerifier::populate(['posts'], $authors);
Оставлю, может кому пригодится.
Ответ написан
Пригласить эксперта
Ответы на вопрос 2
mhthnz
@mhthnz
PHP, YII2, Golang, Linux
Почему бы не воспользоваться IN?
<?

$authors = Author::find()->all();
$authorsIds = ArrayHelper::getColumn($authors, 'id');
$allPosts = Posts::find()->where(['author_id' => $authorsIds])->orderBy('author_id')->all();

##### И далее чтобы вытащить все посты определенного автора

foreach($authors as $author) {
    $author_id = $author->id;
    $posts = ArrayHelper::getColumn($allPosts, function($element) use ($author_id){
        if ($element['author_id'] == $author_id) {
            return $element;
        }
        return false;
    });

    // Записываем в объект author данные связи с posts
    $author->populateRelation('posts', $posts);
}

#### Теперь данные о постах можно спокойно доставать из модели Authors

$author = array_pop($authors);
var_dump($author->posts);


#### Все это можно инкапсулировать за статической функцией, например в классе Authors

public static function fillPosts($authors) 
{
	$authorsIds = ArrayHelper::getColumn($authors, 'id');
	$allPosts = Posts::find()->where(['author_id' => $authorsIds])->orderBy('author_id')->all();
	foreach($authors as $author) {
	    $author_id = $author->id;
	    $posts = ArrayHelper::getColumn($allPosts, function($element) use ($author_id){
	        if ($element['author_id'] == $author_id) {
	            return $element;
	        }
	        return false;
	    });

	    // Записываем в объект author данные связи с posts
	    $author->populateRelation('posts', $posts);
	}
}

### Ну и вызывать

$authors = Author::find()->all();
Author::fillPosts($authors);

Что-то вроди этого должно получиться, код не проверял.
Ответ написан
webinar
@webinar Куратор тега Yii
Учим yii: https://youtu.be/-WRMlGHLgRg
Сделайте в моделе геттер, который вернет то что Вам нужно. В нем реализуйте свою логику. Можно его даже в afterfind засунуть. А вообще сложно по такому примеру понять что конкретно нужно.
Для чего это? Для простоты использования в коде или для оптимизации? Если второе, то тут больше толку будет от использования кеша.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы