Laravel: Возвращение именного владельца полиморфного отношения
Я могу найти ряд обсуждений по этому вопросу, но не ясное решение. Вот две ссылки, хотя я расскажу все в своем собственном вопросе здесь.
Вопросы Github
Обсуждение Laravel.io
Простое объяснение
Это простое объяснение моей проблемы для любого, кто уже знаком с полиморфными отношениями Laravel.
При использовании $morphClass содержимое $morphClass, которое сохраняется в базе данных как тип морфинга, используется для имени класса при попытке найти владельца полиморфного отношения. Это приводит к ошибке, так как вся точка $morphClass заключается в том, что она не является полностью именным именем класса.
Как вы определяете имя класса, которое должно использовать полиморфное отношение?
Подробное пояснение
Это более подробное объяснение, объясняющее, что именно я пытаюсь сделать, и почему я пытаюсь сделать это с примерами.
При использовании Полиморфных отношений в Laravel независимо от того, что сохраняется, поскольку тип "morph_type" в базе данных считается классом.
Итак, в этом примере:
class Photo extends Eloquent {
public function imageable()
{
return $this->morphTo();
}
}
class Staff extends Eloquent {
public function photos()
{
return $this->morphOne('Photo', 'imageable');
}
}
class Order extends Eloquent {
public function photos()
{
return $this->morphOne('Photo', 'imageable');
}
}
База данных будет выглядеть так:
staff
- id - integer
- name - string
orders
- id - integer
- price - integer
photos
- id - integer
- path - string
- imageable_id - integer
- imageable_type - string
Теперь первая строка фотографий может выглядеть так:
идентификатор, путь, imageable_id, imageable_type
1, image.png, 1, персонал
Теперь я могу либо получить доступ к Photo из модели Staff, либо к сотруднику из фотомодели.
//Find a staff member and dump their photos
$staff = Staff::find(1);
var_dump($staff->photos);
//Find a photo and dump the related staff member
$photo = Photo::find(1);
var_dump($photo->imageable);
Пока все хорошо. Однако, когда я их простаиваю, я сталкиваюсь с проблемой.
namespace App/Store;
class Order {}
namespace App/Users;
class Staff {}
namespace App/Photos;
class Photo {}
Теперь, что сохранено в моей базе данных:
идентификатор, путь, imageable_id, imageable_type
1, image.png, 1, App/Пользователи/Staff
Но я не хочу этого. Это ужасная идея иметь полные имена имен имен, сохраненные в базе данных!
К счастью, Laravel имеет возможность установить переменную $morphClass. Например:
class Staff extends Eloquent {
protected $morphClass = 'staff';
public function photos()
{
return $this->morphOne('Photo', 'imageable');
}
}
Теперь строка в моей базе данных будет выглядеть так, что это потрясающе!
идентификатор, путь, imageable_id, imageable_type
1, image.png, 1, персонал
И получение фотографий сотрудника работает абсолютно нормально.
//Find a staff member and dump their photos
$staff = Staff::find(1);
//This works!
var_dump($staff->photos);
Однако полиморфная магия поиска владельца фотографии не работает:
//Find a photo and dump the related staff member
$photo = Photo::find(1);
//This doesn't work!
var_dump($photo->imageable);
//Error: Class 'staff' not found
Предположительно, должен быть способ сообщить полиморфные отношения того, какое имя класса использовать при использовании $morphClass, но я не могу найти никакой ссылки на то, как это должно работать в документах, в исходном коде или через Google.
Любая помощь?
Ответы
Ответ 1
Есть два простых способа: один ниже, другой в ответе @lukasgeiter, предложенный Тейлором Отуэлом, который я, безусловно, также предлагаю проверить:
// app/config/app.php or anywhere you like
'aliases' => [
...
'MorphOrder' => 'Some\Namespace\Order',
'MorphStaff' => 'Maybe\Another\Namespace\Staff',
...
]
// Staff model
protected $morphClass = 'MorphStaff';
// Order model
protected $morphClass = 'MorphOrder';
сделано
$photo = Photo::find(5);
$photo->imageable_type; // MorphOrder
$photo->imageable; // Some\Namespace\Order
$anotherPhoto = Photo::find(10);
$anotherPhoto->imageable_type; // MorphStaff
$anotherPhoto->imageable; // Maybe\Another\Namespace\Staff
Я бы не использовал имена реальных классов (Order
и Staff
), чтобы избежать возможных дубликатов. Там очень мало шансов, что что-то будет называться MorphXxxx
, чтобы оно было достаточно безопасным.
Это лучше, чем хранение пространств имен (я не против взглядов в db, однако было бы неудобно, если вы что-то измените - вместо App\Models\User
используйте Cartalyst\Sentinel\User
и т.д.), потому что все, что вам нужно, это обмен файлами с помощью конфигурации псевдонимов.
Однако есть и недостатки - вы не будете знать, что такое модель, просто проверив db - в случае, если это имеет для вас значение.
Ответ 2
Мне нравится решение @JarekTkaczyks, и я предлагаю вам использовать его. Но, ради полноты, есть еще один способ, который Тейлор кратко упоминает в github
Вы можете добавить атрибут атрибута для атрибута imageable_type
, а затем использовать массив "classmap" для поиска нужного класса.
class Photo extends Eloquent {
protected $types = [
'order' => 'App\Store\Order',
'staff' => 'App\Users\Staff'
];
public function imageable()
{
return $this->morphTo();
}
public function getImageableTypeAttribute($type) {
// transform to lower case
$type = strtolower($type);
// to make sure this returns value from the array
return array_get($this->types, $type, $type);
// which is always safe, because new 'class'
// will work just the same as new 'Class'
}
}
Примечание, что вам все равно понадобится атрибут morphClass
для запроса с другой стороны отношения.
Ответ 3
При использовании Laravel 5.2 (или новее) вы можете использовать новую функцию morphMap
для решения этой проблемы. Просто добавьте это к функции boot
в app/Providers/AppServiceProvider
:
Relation::morphMap([
'post' => \App\Models\Post::class,
'video' => \App\Models\Video::class,
]);
Подробнее об этом: https://nicolaswidart.com/blog/laravel-52-morph-map
Ответ 4
Пусть laravel помещает его в пространство имен db и все. Если вам нужно короткое имя класса для чего-то, кроме того, чтобы сделать вашу базу данных более красивой, тогда определите accessor для чего-то вроде:
<?php namespace App\Users;
class Staff extends Eloquent {
// you may or may not want this attribute added to all model instances
// protected $appends = ['morph_object'];
public function photos()
{
return $this->morphOne('App\Photos\Photo', 'imageable');
}
public function getMorphObjectAttribute()
{
return (new \ReflectionClass($this))->getShortName();
}
}
Я думаю, что в таких сценариях я всегда возвращаюсь к таким сценариям, что Laravel довольно хорошо проверен и работает, как ожидается, по большей части. Зачем бороться с каркасом, если вам это не нужно, особенно если вас просто раздражает пространство имен, находящееся в db. Я согласен, что это не очень хорошая идея, но я также чувствую, что могу тратить свое время на более полезное преодоление и работу над кодом домена.
Ответ 5
Для обеспечения надежной загрузки полиморфных отношений, например Photo::with('imageable')->get();
, необходимо вернуть null, если тип пуст.
class Photo extends Eloquent {
protected $types = [
'order' => 'App\Store\Order',
'staff' => 'App\Users\Staff'
];
public function imageable()
{
return $this->morphTo();
}
public function getImageableTypeAttribute($type) {
// Illuminate/Database/Eloquent/Model::morphTo checks for null in order
// to handle eager-loading relationships
if(!$type) {
return null;
}
// transform to lower case
$type = strtolower($type);
// to make sure this returns value from the array
return array_get($this->types, $type, $type);
// which is always safe, because new 'class'
// will work just the same as new 'Class'
}
}
Ответ 6
Вот как вы можете получить имя класса морфинга (псевдоним) из модели Eloquent:
(new Post())->getMorphClass()
Ответ 7
Для нас проблема морфинга классов была больше связана с тем, что для ее бесперебойной работы каждая модель должна была отображаться в карте отношений при каждом переименовании. Следующие проблемы были тем, с чем мы постоянно сталкивались:
На начальной стадии разработки любого проекта отношения изменяются между различными аспектами проекта, и поэтому всегда что-то в конечном итоге оказывается в его пространстве имен.
Учитывая, что в проекте может быть 100 или даже 1000 таблиц, поддерживать такую карту морфинга было невозможно. Также у нас есть соглашение для именования класса морфинга из имени класса (например, DoctorSpecialization = doctor specialization). Мало кросс-пакетов конфликтующих моделей или вообще нет, есть уникальная.
Если в коде несколько моделей адресовали одну и ту же таблицу, класс morph должен был отличаться, что делало разделение совсем не гибким.
Поэтому мы разработали следующий (супер упрощенный) способ внедрения автоматической генерации имени класса морфа с необязательным стандартным поведением для связи с крайними случаями в нашей внутренней структуре:
class BaseModel extends Model
{
...
public static function getActualClassNameForMorph($class): string
{
$modelClass = parent::getActualClassNameForMorph($class);
if (!class_exists($modelClass)) {
$modelClass = $this->combineWithAppAndModelNamespaces(Str::studly($class));
}
return $modelClass;
}
public function getMorphClass(): string
{
$class = parent::getMorphClass();
if ($class === static::class) {
do {
$parentClass = $class;
$class = (new ReflectionClass($parentClass))->getParentClass()->getName();
} while ($class !== BaseModel::class);
$class = Str::lower(CaseConverters::studlyToTitle((new ReflectionClass($parentClass))->getShortName()));
}
return $class;
}
...
}
На следующей итерации мы планируем создать кэш памяти в Redis для рабочих развертываний.