2016-02-16 6 views
1

У меня есть яркое отношение к моей модели Laravel, которая является динамической, то есть значение определенного поля базы данных определяет, какая модель будет загружена. Я могу загрузить эту связь, когда я впервые создаю экземпляр модели, а затем ссылаюсь на отношение, но это не работает, когда я хочу загрузить эти отношения.Динамические отношения Laravel - атрибуты модели доступа при активной загрузке

В частности, у меня есть модель Product. Этот продукт может быть или не быть родителем другого продукта. Если parent_id продукта установлен в 0, тогда этот продукт считается родительской частью (с детьми или нет). Если значение parent_id установлено на идентификатор другого продукта, то этот продукт является дочерним. Мне нужно иметь доступ к Product::with('parent') и знать, что отношение parent будет возвращаться с либо самим (да, дублированными данными), либо другим продуктом, если это ребенок.

Вот мои отношения до сих пор:

public function parent() 
{ 
    if ($this->parent_id > 0) { 
     return $this->belongsTo('App\Product', 'parent_id', 'id'); 
    } else { 
     return $this->belongsTo('App\Product', 'id', 'id'); 
    } 
} 

Когда я жадная загрузка, $this->parent_id всегда неопределенный, и поэтому это отношение будет только когда-либо вернуть себе, даже если он на самом деле является родительским продуктом.

Есть ли способ получить доступ к атрибутам модели до отношение загружается? Я думал о работе в отдельном запросе, прежде чем возвращать отношение, но я понял, что у меня нет доступа к идентификатору продукта, чтобы даже запустить этот запрос.

Если это невозможно, каковы некоторые другие способы решения этой проблемы? Не кажется, что это можно решить с помощью традиционной полиморфной взаимосвязи. У меня только две возможные идеи:

  • Добавить какое-то ограничение в отношение belongsTo, где я динамически определяю внешний ключ.
  • Создайте собственные пользовательские отношения, которые используют внешний ключ на основе другого поля базы данных.

Я, честно говоря, понятия не имею, как я мог бы применить любой из этих. Правильно ли я это делаю? Есть ли что-то, что я пропускаю?


Поразмыслив над этим больше, я думаю, что самый простой способ поставить вопрос: есть ли способ для динамического выбора внешнего ключа для отношений внутри самого отношения во время выполнения? Мои варианты использования не позволяют использовать ярые ограничения загрузки при вызове отношения - ограничения должны применяться к самому отношению.

+0

Почему бы не поместить собственный идентификатор продукта в поле, если он не является родителем?? –

+0

Если бы я начинал с нуля, я бы подумал, что сделаю это, но, к сожалению, я создаю службу API Laravel для огромной старой базы данных/базы данных. Если бы я изменил работу этого поля, это сломало бы код в другом месте. Я подумал о добавлении нового поля, которое это делает, но мне нужно добавить тонну устаревшего кода в каждом месте, где продукт может быть введен в устаревший сайт - настоящий кошмарный проект. –

+0

Я надеялся каким-то образом добиться этого, используя Laravel. Если каждая альтернатива окажется невозможной, я буду пинговать вас, и вы ответите так, чтобы хотя бы ваш ответ помог другим сделать то же самое, хотя это мне не помогло. Я собираюсь дать этот вопрос немного больше времени, прежде чем сдаться, хотя. –

ответ

5

Из-за того, как работает загруженная загрузка, нет ничего, что можно действительно сделать для запуска SQL, чтобы выполнить то, что вы ищете.

Когда вы делаете Product::with('parent')->get(), он выполняет два запроса.

Во-первых, он выполняет запрос, чтобы получить все продукты:

select * from `products` 

Далее он выполняет запрос, чтобы получить нетерпеливые нагруженные родителей:

select * from `products` where `products`.`id` in (?, ?, ?) 

Количество параметров (?) соответствует количеству результатов первого запроса. Как только второй набор моделей был восстановлен, функция match() используется для связывания объектов друг с другом.

Чтобы сделать то, что вы хотите, вам нужно будет создать новое отношение и переопределить метод match(). Это будет обрабатывать нетерпеливый аспект загрузки. Кроме того, вам необходимо переопределить метод для обработки ленивого аспекта загрузки.

Во-первых, создать пользовательский класс отношений:

class CustomBelongsTo extends BelongsTo 
{ 
    // Override the addConstraints method for the lazy loaded relationship. 
    // If the foreign key of the model is 0, change the foreign key to the 
    // model's own key, so it will load itself as the related model. 

    /** 
    * Set the base constraints on the relation query. 
    * 
    * @return void 
    */ 
    public function addConstraints() 
    { 
     if (static::$constraints) { 
      // For belongs to relationships, which are essentially the inverse of has one 
      // or has many relationships, we need to actually query on the primary key 
      // of the related models matching on the foreign key that's on a parent. 
      $table = $this->related->getTable(); 

      $key = $this->parent->{$this->foreignKey} == 0 ? $this->otherKey : $this->foreignKey; 

      $this->query->where($table.'.'.$this->otherKey, '=', $this->parent->{$key}); 
     } 
    } 

    // Override the match method for the eager loaded relationship. 
    // Most of this is copied from the original method. The custom 
    // logic is in the elseif. 

    /** 
    * Match the eagerly loaded results to their parents. 
    * 
    * @param array $models 
    * @param \Illuminate\Database\Eloquent\Collection $results 
    * @param string $relation 
    * @return array 
    */ 
    public function match(array $models, Collection $results, $relation) 
    { 
     $foreign = $this->foreignKey; 

     $other = $this->otherKey; 

     // First we will get to build a dictionary of the child models by their primary 
     // key of the relationship, then we can easily match the children back onto 
     // the parents using that dictionary and the primary key of the children. 
     $dictionary = []; 

     foreach ($results as $result) { 
      $dictionary[$result->getAttribute($other)] = $result; 
     } 

     // Once we have the dictionary constructed, we can loop through all the parents 
     // and match back onto their children using these keys of the dictionary and 
     // the primary key of the children to map them onto the correct instances. 
     foreach ($models as $model) { 
      if (isset($dictionary[$model->$foreign])) { 
       $model->setRelation($relation, $dictionary[$model->$foreign]); 
      } 
      // If the foreign key is 0, set the relation to a copy of the model 
      elseif($model->$foreign == 0) { 
       // Make a copy of the model. 
       // You don't want recursion in your relationships. 
       $copy = clone $model; 

       // Empty out any existing relationships on the copy to avoid 
       // any accidental recursion there. 
       $copy->setRelations([]); 

       // Set the relation on the model to the copy of itself. 
       $model->setRelation($relation, $copy); 
      } 
     } 

     return $models; 
    } 
} 

После того как вы создали свой собственный класс отношений, вам необходимо обновить модель, чтобы использовать этот обычай отношения. Создайте новый метод на вашей модели, который будет использовать ваши новые отношения CustomBelongsTo, и обновите свой метод отношений parent(), чтобы использовать этот новый метод, а не базовый метод belongsTo().

class Product extends Model 
{ 

    // Update the parent() relationship to use the custom belongsto relationship 
    public function parent() 
    { 
     return $this->customBelongsTo('App\Product', 'parent_id', 'id'); 
    } 

    // Add the method to create the CustomBelongsTo relationship. This is 
    // basically a copy of the base belongsTo method, but it returns 
    // a new CustomBelongsTo relationship instead of the original BelongsTo relationship 
    public function customBelongsTo($related, $foreignKey = null, $otherKey = null, $relation = null) 
    { 
     // If no relation name was given, we will use this debug backtrace to extract 
     // the calling method's name and use that as the relationship name as most 
     // of the time this will be what we desire to use for the relationships. 
     if (is_null($relation)) { 
      list($current, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); 

      $relation = $caller['function']; 
     } 

     // If no foreign key was supplied, we can use a backtrace to guess the proper 
     // foreign key name by using the name of the relationship function, which 
     // when combined with an "_id" should conventionally match the columns. 
     if (is_null($foreignKey)) { 
      $foreignKey = Str::snake($relation).'_id'; 
     } 

     $instance = new $related; 

     // Once we have the foreign key names, we'll just create a new Eloquent query 
     // for the related models and returns the relationship instance which will 
     // actually be responsible for retrieving and hydrating every relations. 
     $query = $instance->newQuery(); 

     $otherKey = $otherKey ?: $instance->getKeyName(); 

     return new CustomBelongsTo($query, $this, $foreignKey, $otherKey, $relation); 
    } 
} 

Справедливое предупреждение, все это не было протестировано.

+1

Вау, большое вам спасибо! Мне никогда не удавалось строить свои собственные отношения раньше, но это делает именно то, что мне нужно! Тем не менее, я хочу указать, что в конкретном случае это не будет работать: если вы используете это в вложенном запросе 'whereHas', то есть' Product :: whereHas ('parent.location', function ($ q) { // something}) -> find (1234) '. Тем не менее, я думаю, что это больше связано с недостатком того факта, что он строит подзапросы на себя с другими полями, необходимыми как первичный ключ, а не самим этим отношением (если это имеет смысл). Просто хотел добавить его здесь, если кому-то это понадобится. Спасибо! –

+1

@ У AndyNoelker Laravel возникла проблема с 'has' и' whereHas' на собственных отношениях. Я фактически недавно представил PR для 5.1 и 5.2, которые были объединены, чтобы исправить эту проблему. Если вы на 5.1, версия 5.1.30 была только что отмечена сегодня около 40 минут назад, у которой есть исправление. Если вы в 5.2, версия 5.2.15 была отмечена 5 дней назад, у которой есть исправление. Если вам нужна эта функция 'whereHas', вам понадобится обновить композитор Laravel. – patricus

+1

@ AndyNoelker Если вы заботитесь о деталях gory, вы можете проверить [PR # 12146] (https://github.com/laravel/framework/pull/12146). – patricus

Смежные вопросы