Poznajemy polimorficzne odpowiedniki wcześniej poznanych relacji jeden do jednego oraz jeden do wielu. Kontynuujemy zgłębianie zagadnień bazodanowych, ORMa Laravela oraz Query Buildera i fasady DB.
Tworzymy polimorficzną relację jeden do jednego – kupiec, kupujący, dane kontaktowe
Będziemy mieli modele dla kupca i sprzedającego. Każdy z nich będzie w relacji jeden do jednego z modelem danych kontaktowych. W relacji niepolimorficznej musielibyśmy tworzyć osobno model danych kontaktowych kupca, model danych kontaktowych sprzedającego i między nimi relacje tworzyć.
W relacji polimorficznej osiągniemy to samo za pomocą jednego modelu dane kontaktowe, który można potem podłączyć do dowolnej ilości modeli.
Tworzymy model Buyer wraz z migracją, fabryką i seederem:
php artisan make:model Buyer -mfs
Przechodzimy do migracji i wypełniamy pola, jakie chcemy mieć w naszym modelu:
//buyer migration
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('buyers', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('name');
$table->integer('age');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('buyers');
}
};
Wykonujemy migrację:
php artisan migrate
Tabela 'buyers’ z polami ID, timestampami oraz polami 'name’ i 'age’ jest już poprawnie utworzona w bazie danych. Pora utworzyć model Seller – z migracją, fabryką i seederem:
php artisan make:model Seller -mfs
Migracja sprzedającego będzie wyglądać podobnie:
//seller migration
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('sellers', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('name');
$table->integer('age');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('sellers');
}
};
Migrujemy i tworzymy model Contact wraz z migracją:
php artisan migrate
php artisan make:model Contact -m
Wypełniamy pola jakie ma mieć nasz kontakt plus tworzymy interfejs 'contactable’, którego będziemy używać do tworzenia relacji z innymi modelami:
//contact migration
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('contacts', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('city')->nullable(false);
$table->string('street')->nullable(false);
$table->morphs('contactable');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('contacts');
}
};
Ten interfejs będzie posiadać (w bazie danych) pola 'contactable_type’ oraz 'contactable_id’, poza polami 'city’, 'street’, id oraz timestampami. Dzięki temu można tam przechować zarówno typ (np. kupujący albo sprzedający albo inny model) jak i id modelu.
Na razie jednak odpowiednio wszystko połączmy. Po wykonaniu migracji idziemy do modelu Buyer wypełnić fillable i stworzyć relację z kontaktem:
class Buyer extends Model
{
use HasFactory;
protected $fillable = ['name', 'age'];
public function contact()
{
return $this->morphOne('App\Models\Contact', 'contactable');
}
}
Metoda ->morphOne() to polimorficzny odpowiednik hasOne(). Podajemy namespace modelu, który mamy posiadać, oraz interfejsu. To samo wykonujemy w modelu sprzedającego:
class Seller extends Model
{
use HasFactory;
protected $fillable = ['name', 'age'];
public function contact()
{
return $this->morphOne('App\Models\Contact', 'contactable');
}
}
Sprzedający również posiada jeden kontakt. Ale jest to relacja polimorficzna, kontakty są dzielone między modelami – to jest model jest dzielony. Nie mamy modelów BuyerContact ani SellerContact – mamy jeden generyczny.
Cały czas jednak zarówno jeden Buyer może posiadać jeden kontakt i odwrotnie jak i jeden Seller może posiadać jeden kontakt i odwrotnie. Kontakt jest w tym wypadku dzieckiem dla modelu rodzica i musimy jeszcze w jego modelu poza fillable zaznaczyć polimorficzny odpowiednik ->belongsTo:
class Contact extends Model
{
use HasFactory;
protected $fillable = ['city', 'street'];
public function contactable()
{
return $this->morphTo();
}
}
W ->belongsTo() podawaliśmy, do jakiej klasy modelu nasz model należy, ale tutaj nie musimy – kontakt nie wie, do czego zostanie przypięty, do sprzedającego, kupującego albo jeszcze innego modelu.
Wie tylko, że znajduje się w relacji polimorficznej 1 do 1 jako model-dziecko.
Tworzymy fabryki i seedery dla naszych kupców i sprzedających
To będzie bardzo powtarzalna robota. Po pierwsze, definiujemy fabrykę kupującego:
class BuyerFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'age' => random_int(18,30)
];
}
}
Po drugie, definiujemy fabrykę sprzedającego:
class SellerFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'name' => fake()->name(),
'age' => random_int(18,30)
];
}
}
Następnie, pamiętając o imporcie modelu, ustalamy seeder dla kupującego:
use App\Models\Buyer;
class BuyerSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Buyer::factory()
->count(10)
->create();
}
}
W podobny sposób ustalamy seeder sprzedającego:
use App\Models\Seller;
class SellerSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Seller::factory()
->count(10)
->create();
}
}
Teraz wykonujemy seeding komendami db:seed z flagą zawierającą nazwę klasy każdego seedera:
php artisan db:seed --class=BuyerSeeder;
php artisan db:seed --class=SellerSeeder;
Gotowe – w bazie danych powinniśmy już widzieć fejkowe rekordy.
Podstawowe operacje na naszych modelach – piszemy komendy
Musimy powoli zwiększać nasze umiejętności bazo-danowe oraz ORMowe. Dlatego oprócz naszych modeli, w console.php zaimportujemy sobie także fasadę DB:
(...)
use Illuminate\Support\Facades\DB;
(...)
use App\Models\Buyer;
use App\Models\Seller;
use App\Models\Contact;
Tworzymy komendę logującą ilość sprzedawców w bazie danych:
Artisan::command('buyercount', function () {
$cnt = Buyer::count();
$this->comment("There are $cnt buyers in database");
});
Teraz ta sama komenda, ale z użyciem fasady DB. Nasza tabela nazywa się 'buyers’, zaś komenda wygląda tak:
Artisan::command('buyercount2', function () {
$cnt = DB::table('buyers')->count();
$this->comment("There are $cnt buyers in database");
});
Analogicznie tworzymy komendę logującą ilość sprzedawców:
Artisan::command('sellercount', function () {
$cnt = Seller::count();
$this->comment("There are $cnt sellers in database");
});
Teraz za pomocą fasady DB, którą lepiej poznamy w następnych odcinkach tutoriala:
Artisan::command('sellercount2', function () {
$cnt = DB::table('sellers')->count();
$this->comment("There are $cnt sellers in database");
});
Teraz komenda logująca ilość kupujących i sprzedających posiadających dane kontaktowe:
Artisan::command('hascontact', function () {
$bcnt = Buyer::has('contact')->count();
$scnt = Seller::has('contact')->count();
$this->comment("There are $bcnt buyers with contact");
$this->comment("There are $scnt sellers with contact");
});
Teraz analogicznie komenda pokazująca tych, którzy danych kontaktowych nie posiadają. Moglibyśmy bawić się w odejmowanie od liczby wszystkich tych, którzy posiadają, ale warto chyba teraz poznać metodę doesnthave:
Artisan::command('hasnocontact', function () {
$bcnt = Buyer::doesnthave('contact')->count();
$scnt = Seller::doesnthave('contact')->count();
$this->comment("There are $bcnt buyers with contact");
$this->comment("There are $scnt sellers with contact");
});
Używamy relacji polimorficznej – pierwsza próba
Na początku napiszemy sobie komendę, która loguje jakieś informacje o wybranym sprzedawcy:
Artisan::command('buyerinfo {id}', function (int $id) {
$buyer = Buyer::with('contact')->find($id);
$this->comment("Name: {$buyer->name}");
});
Teraz komenda, pozwalająca nam zapisać do istniejącego sprzedawcy jego dane kontaktowe:
Artisan::command('buyeraddcontact {id}', function (int $id) {
$buyer = Buyer::find($id);
$city = $this->ask('Enter city: ');
$street = $this->ask('Enter street: ');
$this->comment("Name: {$buyer->name}");
$this->comment("City: $city");
$this->comment("Street: $street");
if ($this->confirm('Do you wish to continue?', true)) {
$buyer->contact()->save(
Contact::make(['city' => $city, 'street' => $street])
);
}
});
Korzystamy na naszym modelu-dziecku ze statycznej metody make, która tworzy ten model w pamięci (nie zapisuje, nie ma też wypełnionych pól 'contactable_type’ oraz 'contactable_id’.
Przekazujemy utworzony przez make, niekompletny i z niczym niepołączony kontakt do $buyer->contact()->save(), która to metoda nadaje odpowiednie 'contactable_type’ (nasz typ, czyli buyer) oraz 'contactable_id’ (id naszego buyera) i zapisuje do bazy danych kontakt połączony z modelem Buyer oraz jego instancją o podanym ID.
Możemy teraz zapisać sobie do bazy danych kontakt dla jakiegoś istniejącego buyera a następnie zmodyfikować komendę logującą:
Artisan::command('buyerinfo {id}', function (int $id) {
$buyer = Buyer::with('contact')->find($id);
$this->comment("Name: {$buyer->name}");
$buyer->contact && $this->comment("City: {$buyer->contact->city}");
$buyer->contact && $this->comment("Street: {$buyer->contact->street}");
});
Teraz komenda logująca wypisuje imię sprzedającego oraz – jeżeli ten dane kontaktowe posiada – jego dane kontaktowe. Możemy dodać dane kontaktowe i sprawdzić, że zostały przyznane oraz porównać z jakimś sprzedawcą, któremu takich danych nie przypisaliśmy.
Używamy relacji polimorficznej na innym modelu – sprzedający i kontakt
Cała ta nasza zabawa byłaby niewiele warta, gdyby można było kontakt dopisywać do tylko jednego modelu. Wtedy skorzystalibyśmy z relacji niepolimorficznej 1 do 1 i tworzyli modele BuyerContact, SellerContact i tak dalej.
My jednak mamy relację polimorficzną. Na początek napiszmy funkcje logującą pojedynczego sellera:
Artisan::command('sellerinfo {id}', function (int $id) {
$seller = Seller::with('contact')->find($id);
$this->comment("Name: {$seller->name}");
$seller->contact && $this->comment("City: {$seller->contact->city}");
$seller->contact && $this->comment("Street: {$seller->contact->street}");
});
Teraz analogicznie tworzymy funkcję, która połączy sellera z kontaktem:
Artisan::command('selleraddcontact {id}', function (int $id) {
$seller = Seller::find($id);
$city = $this->ask('Enter city: ');
$street = $this->ask('Enter street: ');
$this->comment("Name: {$seller->name}");
$this->comment("City: $city");
$this->comment("Street: $street");
if ($this->confirm('Do you wish to continue?', true)) {
$seller->contact()->save(
Contact::make(['city' => $city, 'street' => $street])
);
}
});
Contact::make tak samo tworzy niezapisany model kontakt z pewnymi polami wypełnionymi, ale pustym 'contactable_type’ oraz 'contactable_id’, ale teraz przekazujemy go do $seller->contact->save().
Tym razem 'contactable_type’ wskazuje na model Seller zaś 'contactable_id’ – na id naszego sprzedającego.
Mamy zatem 3 modele:
- Sprzedający w relacji 1 do 1 rodzic-dziecko w stosunku do Kontaktu
- Kupujący w relacji 1 do 1 rodzic-dziecko w stosunku do Kontaktu
- Kontakt w relacji 1 do 1 dziecko-rodzic w stosunku do dowolnego modelu, jako że posiada pole 'contactable_type’, które może przyjąć dowolny model oraz pole 'contactable_id’ dla id instancji tego modelu – jedyna restrykcja jest taka, że jeden kontakt może być posiadany tylko przez 1 rodzica.
Relacja polimorficzna jeden do wielu – film, książka i recenzja
Tworzymy model książki razem z migracją, fabryką i seederem:
php artisan make:model Book -mfs
Ustalamy pola książki w jej migracji:
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('books', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('title');
$table->integer('pages');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('books');
}
};
Migrujemy zmiany i tworzymy model filmu razem z migracją, fabryką i seederem:
php artisan migrate;
php artisan make:model Movie -mfs;
Wypełniamy pola w migracji:
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('movies', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('title');
$table->integer('length');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('movies');
}
};
Migrujemy zmiany i tworzymy model recenzji wraz z migracją:
php artisan migrate;
php artisan make:model Review -m;
Wypełniamy pola migracji recenzji oraz tworzymy interfejs 'reviewable’:
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('reviews', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->text('desc');
$table->integer('grade');
$table->morphs('reviewable');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('reviews');
}
};
Migrujemy i przechodzimy do modelu Book, wypełnić fillable oraz stworzyć relację jeden do wielu polimorficzną z interfejsem 'reviewable’:
class Book extends Model
{
use HasFactory;
protected $fillable = ['title', 'pages'];
public function reviews()
{
return $this->morphMany('App\Models\Review', 'reviewable');
}
}
Książka – rodzic, posiada wiele dzieci recenzji, inne modele też mogą posiadać recenzje a zatem relacja polimorficzna, morphMany zamias hasMany, namespace i nazwa interfejsu zamiast nazwy klasy.
To samo w modelu Movie:
class Movie extends Model
{
use HasFactory;
protected $fillable = ['title', 'length'];
public function reviews()
{
return $this->morphMany('App\Models\Review', 'reviewable');
}
}
Teraz pora utworzyć połączenie ze strony dziecka, czyli recenzji. Recenzja może mieć jednego rodzica, ale dowolnego typu, zapewniając interfejs 'reviewable’:
class Review extends Model
{
use HasFactory;
protected $fillable = ['desc', 'grade'];
public function reviewable()
{
return $this->morphTo();
}
}
morphTo – odpowiednik belongsTo, ale recenzja nie wie, do jakiej klasy należy – może należeć do dowolnej, taka istota relacji polimorficznych. Jesteśmy gotowi iść dalej.
Tworzymy fabryki i seedery – książka, film
Fabryka książek:
class BookFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'title' => fake()->sentence(4),
'pages' => random_int(50,500)
];
}
}
Fabryka filmów:
class MovieFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'title' => fake()->sentence(4),
'length' => random_int(90,160)
];
}
}
Seeder książek:
use App\Models\Book;
class BookSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Book::factory()
->count(10)
->create();
}
}
Seeder filmów:
use App\Models\Movie;
class MovieSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
Movie::factory()
->count(10)
->create();
}
}
Komendy:
php artisan db:seed --class=MovieSeeder;
php artisan db:seed --class=BookSeeder;
W przyszłości poznamy bardziej zaawansowane patenty na seeding, na razie nam to musi wystarczyć. Po wykonaniu komend widzimy fejkowe rekordy w bazie danych.
Używamy relacji – zapisanie jednej recenzji
Na początek napiszemy sobie funkcję logującą informacje o książce, bo to na niej przećwiczymy sobie dodawanie jednej recenzji:
Artisan::command('bookinfo {id}', function (int $id) {
$book = Book::withCount('reviews')->find($id);
$this->comment("Title: {$book->title}");
$this->comment("Pages: {$book->pages}");
$this->comment("Reviews: {$book->reviews_count}");
});
Teraz komenda zapisująca recenzję do książki metodą create na metodzie reviews:
Artisan::command('bookaddreview {id}', function (int $id) {
$book = Book::find($id);
$desc = $this->ask('Enter desc: ');
$grade = (int)$this->ask('Enter grade: ');
$this->comment("Title: {$book->title}");
$this->comment("Desc: $desc");
$this->comment("Grade: $grade");
if ($this->confirm('Do you wish to continue?', true)) {
$book->reviews()->create(
['desc' => $desc, 'grade' => $grade]
);
}
});
Jeżeli chcemy być bardziej dokładni, możemy użyć modelu Review i make jak w poprzednim przykładzie relacji 1 do 1:
Artisan::command('bookaddreview {id}', function (int $id) {
$book = Book::find($id);
$desc = $this->ask('Enter desc: ');
$grade = (int)$this->ask('Enter grade: ');
$this->comment("Title: {$book->title}");
$this->comment("Desc: $desc");
$this->comment("Grade: $grade");
if ($this->confirm('Do you wish to continue?', true)) {
$book->reviews()->save(
Review::make(['desc' => $desc, 'grade' => $grade])
);
}
});
Możemy jeszcze usprawnić naszą komendę logującą, aby pokazywała średnią ilość ocen pod warunkiem, że książka ma jakieś recenzje:
Artisan::command('bookinfo {id}', function (int $id) {
$book = Book::withCount('reviews')->find($id);
$this->comment("Title: {$book->title}");
$this->comment("Pages: {$book->pages}");
$this->comment("Reviews: {$book->reviews_count}");
if($book->reviews_count > 0){
$avg = $book->reviews()->avg('grade');
$this->comment("Average grade: {$avg}");
}
});
Używanie relacji – zapisywanie wielu recenzji
Tworzymy naszą komendę logującą info o filmie, bo na nim sobie wypróbujemy dodawanie wielu recenzji na raz:
Artisan::command('movieinfo {id}', function (int $id) {
$movie = Movie::withCount('reviews')->find($id);
$this->comment("Title: {$movie->title}");
$this->comment("Length: {$movie->length}");
$this->comment("Reviews: {$movie->reviews_count}");
if($movie->reviews_count > 0){
$avg = $movie->reviews()->avg('grade');
$this->comment("Average grade: {$avg}");
}
});
Teraz komenda, która obrazuje użycie saveMany na filmie:
Artisan::command('moviereviews {id}', function (int $id) {
$movie = Movie::find($id);
$movie->reviews()->saveMany([
Review::make(['desc' => "Great book", 'grade' => 5]),
Review::make(['desc' => "Average book", 'grade' => 3]),
Review::make(['desc' => "Worst book", 'grade' => 1]),
]);
});
To samo możemy zrobić z książką ponieważ relacja jest polimorficzna:
Artisan::command('bookreviews {id}', function (int $id) {
$book = Book::find($id);
$book->reviews()->saveMany([
Review::make(['desc' => "Great book", 'grade' => 5]),
Review::make(['desc' => "Average book", 'grade' => 3]),
Review::make(['desc' => "Worst book", 'grade' => 1]),
]);
});
A zatem zarówno książka może mieć wiele recenzji jak i film może mieć wiele recenzji zaś recenzja jest polimorficzna – można ją podpinać zarówno do książek jak i filmów, ale jedna recenzja – jeden rodzic. Z drugiej strony relacji – jeden rodzic może mieć wiele dzieci (recenzji).
Wyliczanie średniej – piszemy kilka komend
Dla ćwiczenia piszemy sobie komendę wyliczającą średnią ilość stron w książce z naszej kolekcji:
Artisan::command('pagesavg', function () {
$avg = Book::avg('pages');
$this->comment("Average pages count is $avg");
});
Teraz to samo, ale dla filmów i ich długości:
Artisan::command('lenavg', function () {
$avg = Movie::avg('length');
$this->comment("Average movie length is $avg");
});
Teraz to samo, ale z użyciem fasady DB:
Artisan::command('lenavg2', function () {
$avg = DB::table('movies')->avg('length');
$this->comment("Average movie length is $avg");
});
Okej, spróbujmy sobie teraz napisać funkcję, która dla naszej polimorficznej recenzji wyświetla średnią ilość ocen:
Artisan::command('averagereview', function () {
$avg = Review::avg('grade');
$this->comment("Average grade is $avg");
});
Okej, a teraz pomyślmy, co by się stało, gdybyśmy chcieli napisać średnią ilość ocen dla wszystkich recenzji wszystkich książek, i tylko książek. Średnia ilość ocen pojedynczej książki wygląda tak:
Artisan::command('singlebookavg {id}', function (int $id) {
$avg = Book::find($id)->reviews->avg('grade');
$this->comment("Average review grade for book with id $id is $avg");
});
My jednak chcemy czegoś innego – średnia wszystkich ocen dla wszystkich książek, ale tylko książek. Warto się przyjrzeć jak wygląda tabela reviews w bazie danych.
Mamy tam poza 'desc’ i 'grade’ kolumnę 'reviewable_id’ (tam numer id książki/filmu) oraz kolumnę 'reviewable_type’ a tam widzę powtarzające się dwa różne typy:
- App\Models\Book
- App\Models\Movie
Zatem komenda, która loguje średnią ocen książek w naszej relacji polimorficznej wygląda tak:
Artisan::command('bookavg', function () {
$avg = DB::table('reviews')
->where('reviewable_type', 'App\Models\Book')
->avg('grade');
$this->comment("Average grade for book is $avg");
});
Napisanie komendy logującej średnią ocen filmów jest równie proste:
Artisan::command('movieavg', function () {
$avg = DB::table('reviews')
->where('reviewable_type', 'App\Models\Movie')
->avg('grade');
$this->comment("Average grade for Movie is $avg");
});
Powinniśmy już powoli zaczynać rozumieć, jak te relacje działają oraz coraz lepiej odnajdywać się w pracy z bazą danych przez Laravela. Oczywiście będziemy jeszcze nad tym pracować.
Relację polimorficzną wiele do wielu poznamy w następnym odcinku na nowym, mniej zaśmieconym projekcie. Wyjaśnimy sobie też kilka innych niezbędnych rzeczy.