V dnešním díle se podíváme na často nepochopené vazby, a to polymorfické (anglicky polymorphic, ale když už to máme i počeštěné, tak proč u toho nezůstat). I když se to může zdát jako něco složitého, tak si ukážeme, že ve výsledku se jedná o celkem jednoduchou záležitost. Samozřejmě si vše ukážeme na konkrétním příkladu.

Polymorfické vazby nám umožňují mít jeden model patřící k více modelům pomocí jediné vazby. Abychom si to více ujasnili, představme si situaci, kde máme model Album a  Song. Uživatelé mohou hlasovat jak pro písničku tak i pro album. Pokud použijeme polymorfické vazby, pak můžeme použít jedinou tabulku upvotes pro obě situace. To se může hodit především v případě, kdybychom ideálně potřebovali vytvořit tabulku album_upvotes a tabulku song_upvotes, abychom rozlišili hlasy. S polymorfickými vazbami nicméně nepotřebujeme vytvářet dvě tabulky. Pojďme se podívat na další příklad, kde si vše vysvětlíme detailněji.

Komentáře

Asi nejčastěji používaným příkladem bývají komentáře, takže proč si neukázat na nich. Mějme například webový portál, který píše o videohrách a filmech. Komentáře mohou uživatele vkládat u videoher i u filmů. Vytvoříme si tedy databázovou strukturu, se kterou budou naše modely pracovat. Databázová struktura (bez sloupců created_at a updated_at) bude vypadat takto:


films
    id - integer
    title - string
    rating - integer

video_games
    id - integer
    title - string
    body - string

comments
    id - integer
    commentable_id - integer
    commentable_type - string

Migrace

Jako v předešlých dílech, jako první začneme s migracemi. Protože budeme následně potřebovat i modely, můžeme pomocí artisanu rovnou vytvořit vše, co budeme potřebovat:


php artisan make:model Comment -m
php artisan make:model VideoGame -m
php artisan make:model Film -m

Flag -m znamená, že se má zároveň s modelem vytvořit i migrace. Nyní si upravíme všechny tři migrace.


// Film
public function up()
{
    Schema::create('films', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title');
        $table->unsignedInteger('rating');
        $table->timestamps();
    });
}

// VideoGame
public function up()
{
    Schema::create('video_games', function (Blueprint $table) {
        $table->increments('id');
        $table->string('title');
        $table->string('body');
        $table->timestamps();
    });
}

// Comment
public function up()
{
    Schema::create('comments', function (Blueprint $table) {
        $table->increments('id');
        $table->morphs('commentable');
        $table->timestamps();
    });
}

Migraci pro film a videohry si myslím není potřeba nějak více popisovat. To hlavní se totiž odehrává v migraci pro komentáře. Všimněte si definice sloupce commentable, kde se používá metoda morphs(). Když bychom se podívali na tuto metodu, zjistili bychom, že obsahuje toto:


// Illuminate/Database/Schema/Blueprint.php

public function morphs($name, $indexName = null)
{
    $this->string("{$name}_type");

    $this->unsignedBigInteger("{$name}_id");

    $this->index(["{$name}_type", "{$name}_id"], $indexName);
}

Ve výsledku tedy metoda morphs() zanořuje vytvoření dalších dvou sloupců, tedy konkrétně v našem případě commentable_type a commentable_id. Jako bonus k tomu vytvoří i index. Jako název sloupců jsem zvolil commentable, protože je potřeba myslet na to, že do těchto sloupců může přijít cokoliv, kde je možné vkládat komentáře (film a videohry), tedy sloupce nejsou specifické pouze pro jeden typ, jsou různorodé (polymorfní). Spustíme si tedy migrace:


php artisan migrate

Eloquent modely

Na polymorfické vazby je potřeba se dívat podobně, jako je například vazba 1:n, kterou jsme si popisovali v dřívějším díle. Jako první si upravíme model Comment:


class Comment extends Model
{
    public function commentable()
    {
        return $this->morphTo();
    }
}

Pokud bychom si představili, že nevyužíváme polymorfickou vazbu, ale vazbu 1:n například k modelu Film, pak by klasická návratová definice vazby byla return $this->belongsTo(Film::class);. Avšak v tomto případě používáme polymorfismus, takže není možné přesně specifikovat vazbu, ke kterému modelu komentář patří. Proto namísto belongsTo použijeme morphTo a také nespecifikujeme žádný konkrétní model, protože tam může přijít cokoliv, co je "commentable".

Dále si upravíme modely Film a VideoGame, které budou mít shodné vazby:


class Film extends Model
{
    protected $fillable = ['title', 'rating'];   

    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

class VideoGame extends Model
{
    protected $fillable = ['title', 'body'];

    public function comments()
    {
        return $this->morphMany(Comment::class, 'commentable');
    }
}

Podobně jako v případě 1:n namísto metody hasMany, použijeme metodu morphMany. Jako první parametr uvedeme na jaký model se chceme připojovat a jako druhý parametr je potřeba přesně specifikovat název polymorfismu, jaký jsme si definovali v migraci pro tabulku comments.

Uložení záznamu

Ukládání záznamů se provádí úplně totožně jako v případě vazby 1:n. Abych to zkrátil, použil jsem tinker pro vytvoření filmu, videohry a komentáře:


php artisan tinker
>>> $film = App\Film::create(['title' => 'Fancy film', 'rating' => 5]);
>>> $videoGame = App\VideoGame::create(['title' => 'Super video game', 'body' => 'Greatest game ever']);
>>> $commentOne = new App\Comment;
>>> $commentTwo = new App\Comment;
>>> $commentThree = new App\Comment;
>>> $film->comments()->save($commentOne);
>>> $videoGame->comments()->save($commentTwo);
>>> $film->comments()->save($commentThree);

Když bychom se podívali do databáze, tak kromě nového filmu a videohry bychom našli tři komentáře:

test

Získávání záznamu

Pokud bychom chtěli získat všechny komentáře pro daný film, můžeme využít naší vazby:


$film = App\Film::find(1);
$film->comments;

// vysledek
=> Illuminate\Database\Eloquent\Collection {#2914
     all: [
       App\Comment {#2903
         id: 1,
         commentable_type: "App\Film",
         commentable_id: 1,
         created_at: "2018-11-10 10:33:49",
         updated_at: "2018-11-10 10:33:49",
       },
       App\Comment {#2898
         id: 3,
         commentable_type: "App\Film",
         commentable_id: 1,
         created_at: "2018-11-10 10:34:52",
         updated_at: "2018-11-10 10:34:52",
       },
     ],
   }

Jde to i obráceně, pokud bychom chtěli získat vlastníka polymorfické vazby z polymorfického modelu na základě názvu metody, která vykonává vazbu morpTo. Tedy v našem případě pokud bychom chtěli získat film, ke kterému komentář patří:


$comment = App\Comment::find(1);
$comment->commentable;

// vysledek
=> App\Film {#2894
     id: 1,
     title: "Fancy film",
     rating: 5,
     created_at: "2018-11-10 10:29:40",
     updated_at: "2018-11-10 10:29:40",
   }

Stejný postup pak můžeme použít i u videoher.

Vlastní typ

Laravel defaultně používá celý název třídy, který ukládá do databáze. V našem případě tedy do tabulky comments do sloupce commentable_type ukládá hodnotu App\Film nebo App\VideoGame. V některých případech toto může být celkem probém, například pokud bychom změnili namespace, museli bychom vytvořit i migraci, která upraví všechny záznamy v databázi. Stejně tak pokud třeba používáme dlouhý namespace pro model (např. App\Models\Blog\Neco\NecoJineho), pak bychom museli upravit typ sloupce, abych dokázal vkládat i velké záznamy.

Abychom se těmto problémům vyhnuli, můžeme využít metodu morphMap. Tato metoda dá vědět Eloquentu, aby namísto názvu třídy použil náš vlastní název. Metodu morphMap je možné zaregistrovat ve funkci boot() v AppServiceProvider nebo si můžeme vytvořit vlastní service provider:


use Illuminate\Database\Eloquent\Relations\Relation;    

public function boot()
{
    Relation::morphMap([
       'film' => \App\Film::class,
       'videoGame' => \App\VideoGame::class 
    ]);
}

Po změně nezapomeňte zavolat příkaz composer dumpautoload, aby se zaregistrovaly změny. Poté můžete znovu zkusit uložit nový záznam, kde již nebude celý název třídy, ale pouze alias, který jsme si definovali.

V dalším, posledním, díle si vysvětlíme opět polymorfické vazby, tentokrát však v relaci n:n, neboli many to many.

Není vám něco jasné, mám někde chybu nebo chcete přidat svůj názor na článek? Napište do komentářů pod článkem.