Подтвердить что ты не робот

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.

Любая помощь?

4b9b3361

Ответ 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 для рабочих развертываний.