Uczymy się korzystać z traits (element języka PHP), aby mieć lepszy porządek w relacjach Laravela, więcej DRY i więcej modularności i czystego kodu. Do dzieła.
Ok, rzućmy okiem na Review, te rzeczy wywalimy:
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
public function latestComment()
{
return $this->morphOne(Comment::class, 'commentable')->latestOfMany();
}
public function oldestComment()
{
return $this->morphOne(Comment::class, 'commentable')->oldestOfMany();
}
Z Movie wywalimy te rzeczy, jednocześnie upewniając się że latestComment i oldestComment również tam się znajdzie:
public function image()
{
return $this->morphOne(Image::class, 'imageable');
}
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
Ok, tworzymy w App\Models folder Traits, w nim Image.php, dodajemy namespace, import (ale bez konfliktu) oraz metodę image w trait o takiej samej nazwie:
<?php
namespace App\Models\Traits;
use App\Models\Image as ImageModel;
trait Image {
public function image(){
return $this->morphOne(ImageModel::class, 'imageable');
}
}
Podobnie tworzymy Trait o nazwie Comments:
<?php
namespace App\Models\Traits;
use App\Models\Comment;
trait Comments {
public function comments()
{
return $this->morphMany(Comment::class, 'commentable');
}
public function latestComment()
{
return $this->morphOne(Comment::class, 'commentable')->latestOfMany();
}
public function oldestComment()
{
return $this->morphOne(Comment::class, 'commentable')->oldestOfMany();
}
}
Teraz wywalamy z Review to, co niepotrzebne:
use App\Models\Traits\Comments;
use App\Models\Traits\Image;
class Review extends Model
{
use HasFactory;
use Image, Comments;
protected $fillable = ["author", "content", "rating"];
public function movie(){
return $this->belongsTo(Movie::class);
}
public function tags(){
return $this->belongsToMany(Tag::class)->withTimestamps();
}
}
Podobnie z Movie:
use App\Models\Traits\Comments;
use App\Models\Traits\Image;
class Movie extends Model
{
use HasFactory;
use Image, Comments;
protected $fillable = ["title", "director", "length", "release_date"];
public function reviews(){
return $this->hasMany(Review::class);
}
public function latestReview()
{
return $this->hasOne(Review::class)->latestOfMany();
}
public function oldestReview()
{
return $this->hasOne(Review::class)->oldestOfMany();
}
public function worstReview()
{
return $this->hasOne(Review::class)->ofMany('rating', 'min');
}
public function bestReview()
{
return $this->hasOne(Review::class)->ofMany('rating', 'max');
}
}
Pytanie, czy powinniśmy utworzyć więcej traits? Traits, jeśli jeszcze tego nie ogarnęliśmy, to są mixiny. Nie tworzy się traits dla jednej rzeczy, po prostu się ją trzyma (zatem wszystkie belongsTo i belongsToMany odpadają)
Czy hasOne to dobre miejsce na trait? To zależy. Jeżeli wiele różnych modeli będzie w relacji hasOne do tamtego modelu, to tak.
Druga opcja to gdy mamy więcej tych metod, to w sumie można taki mixin utworzyć, żeby przesadnie nas nie straszyły te metody. Z takim otwarciem się na rozszerzenie, że w razie w, to jeden use i wszystkie te metody lądują w innym modelu.
Jest jeszcze jedna opcja, ale na razie ją pomińmy, sprawdźmy czy wszystko działa oraz czy Movie dostał latestComment i oldestComment:
Artisan::command('m-latest-comment {id}', function (int $id) {
$m = Movie::with('latestComment')->findOrFail($id);
$this->comment("Movie title: {$m->title}");
$this->comment("Movie id: {$m->id}");
$this->comment("Latest comment title: {$m->latestComment->title}");
$this->comment("Latest comment created_at: {$m->latestComment->created_at}");
});
U mnie działa wszystko, sprawdziłem każdą jedną rzecz, każdą jedną komendę, zanim dodałem tę. Też polecam wszystko sprawdzić i upewnić się, że rozumiemy.
Ok, teraz oldest:
Artisan::command('m-oldest-comment {id}', function (int $id) {
$m = Movie::with('oldestComment')->findOrFail($id);
$this->comment("Movie title: {$m->title}");
$this->comment("Movie id: {$m->id}");
$this->comment("Oldest comment title: {$m->oldestComment->title}");
$this->comment("Oldest comment created_at: {$m->oldestComment->created_at}");
});
Takie czary. Zmniejszyliśmy ilość kodu i jednocześnie rozszerzyliśmy funkcjonalności na inne modele.
Ok, pomyślmy co nie jest dobrym kandydatem na trait:
- belongsTo, zawsze będzie na jednym modelu (no chyba, że mamy tych relacji więcej i dzieci więcej w stosunku do jednego rodzica)
- hasOne, no chyba że mamy więcej relacji hasOne z różnymi rodzicami względem tego samego dziecka
- belongsToMany, bo choć istnieje po obu stronach relacji, to w każdej z innym argumentem
- morphTo, zawsze będzie tylko w jednym modelu, model Image, imageable, morphTo, nie ma szans tego zduplikować, typy są w imageable_type, jeden model Image, jedna tabela Images
Traits są po to, aby uprościć. Jeżeli nasze hasOne powtarza się w wielu miejscach, albo mamy relację hasMany i wiele relacji has One of Many, to możemy się na to pokusić.
BelongsTo może się powtarzać, jeżeli wiele różnych modeli pokazuje na tego samego rodzica, aczkolwiek to trochę dziwny case na trait.
Ze swojej konstrukcji, morphTo zawsze będzie jedno, zaś belongsToMany jest w dwóch miejscach, ale z różnymi argumentami.