Poznajemy modele, migracje, podstawy relacji, fabryki i seedery obsługujące modele wraz z relacjami, bawimy się tinkerem ucząc się podstaw ORMa oraz piszemy local query scopes pozwalające nam tworzyć własne dodatki do ORMowych queries.

Model, migracja, fabryka i seeder – prosty przykład modelu Post

Stworzymy prosty model, mający odzwierciedlać post na jakimś blogu, razem z migracją, fabryką i seederem. Tworzyć projekty już umiemy, teraz w dowolnym z nich korzystamy z komendy:

php artisan make:model Post -mfs

Otwieramy migrację i dodajemy pola tekstowe dla autora oraz treści:

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->id();
            $table->string('author');
            $table->text('post');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('posts');
    }
};

Migrujemy znaną nam komendą:

php artisan migrate

Tabela w bazie danych z odpowiednimi polami już została utworzona. Przechodzimy do modelu i wypełniamy atrybut fillable:

class Post extends Model
{
    use HasFactory;
    protected $fillable = ['author', 'post'];
}

Przechodzimy do fabryki i w oparciu o nasze pola fillable tworzymy definicję, jak mają być tworzone 'fejkowe’ dane do testów:

class PostFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'author' => fake()->name(),
            'post' => 'Lorem ipsum bla bla bla'
        ];
    }
}

Przechodzimy do seedera i ustalamy, jak fabryka ma zostać użyta:

use App\Models\Post;
class PostSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Post::factory()->count(5)->create();
    }
}

Teraz używamy naszego seedera komendą:

php artisan db:seed --class=PostSeeder

W bazie danych widzimy już nasze fejkowe rekordy. Dla potwierdzenia przechodzimy do pliku console.php (obok web.php) i tworzymy komendę, pokazującą ilość postów:

use App\Models\Post;
Artisan::command('postcount', function () {
    $cnt = Post::count();
    $this->comment("There are $cnt posts in database");
})->purpose('Shows post count');

Teraz wykonujemy naszą własną komendę:

php artisan postcount

Widzimy, że wygenerowało nam posty – możemy iść dalej.

Relacja jeden do wielu – modele Post i Komentarz

Tworzymy nowy model – PostComment, czyli komentarz posta. On będzie w relacji do modelu post, który będzie mógł mieć wiele komentarzy, a zatem potrzebujemy migracji, modelu, fabryki, ale już nie seedera – tym zajmować się będzie seeder posta. Komentarzy „oderwanych” od postów tworzyć nie będziemy.

Nasza komenda do utworzenia modelu, migracji i fabryki:

php artisan make:model PostComment -mf

Przechodzimy do naszej migracji i dodajemy pole dla komentarza oraz foreign key dla posta:

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('post_comments', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->text('comment');
            $table->foreignId('post_id')->constrained()
                ->cascadeOnDelete();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('post_comments');
    }
};

Migrujemy. Następnie przechodzimy do modelu PostComment wypełnić fillable oraz zaznaczyć rodzaj relacji względem posta:

use App\Models\Post;

class PostComment extends Model
{
    use HasFactory;
    protected $fillable = ['comment'];
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

Komentarz należy do jakiegoś posta – wydaje się to logiczne. Teraz to samo – czytaj zaznaczenie relacji – potrzebne jest jeszcze w modelu Post:

use App\Models\PostComment;
class Post extends Model
{
    use HasFactory;
    protected $fillable = ['author', 'post'];
    public function comments()
    {
        return $this->hasMany(PostComment::class);
    }
}

Pora ustalić w fabryce jak mają być tworzone fejkowe posty. Zrobimy to bardzo prosto:

class PostCommentFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'comment' => 'lorem ipsum comment bla bla bla'
        ];
    }
}

Teraz przechodzimy do seedera modelu Post i zamieniamy tworzenie 5 postów na tworzenie 10 postów z losową liczbą komentarzy:

use App\Models\Post;
use App\Models\PostComment;
class PostSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Post::factory()->count(10)->create()->each(function($post){
            $random_num = random_int(1,5);
            PostComment::factory()
            ->count($random_num)
            ->for($post)
            ->create();
        });
    }
}

Teraz możemy wykonać seeding używając raz jeszcze komendy:

php artisan db:seed --class=PostSeeder

Następnie możemy sprawdzić, ile mamy postów, naszą własną komendą:

php artisan postcount

Możemy dodać sobie komendę, która podliczy nam posty posiadające przynajmniej 1 komentarz:

Artisan::command('postswherecomments', function () {
    $cnt = Post::has('comments')->count();
    $this->comment("There are $cnt posts with comments");
})->purpose('Shows no posts with comments');

Wywołujemy ją jak każdą customową komendę:

php artisan postswherecomments

Artisan Tinker – próbujemy komendy

Budowę różnych queries Laravelowego ORMa najlepiej wypróbować w tinkerze. Odpalamy go komendą:

php artisan tinker

Teraz spróbujemy złapać wszystkie nasze posty komendą:

$posts = App\Models\Post::all(); 

W tinkerze musimy podawać pełne nazwy namespace, niestety. Ewentualnie możemy zaimportować model (po wyjściu z tinkera oczywiście zapomni):

use App\Models\Post;

Teraz możemy już używać modelu bez namespace. Załadujmy posty, ale z ilością komentarzy:

$postsWithCount = Post::withCount('comments')->get();  

Teraz nasze posty mają poza swoją treścią pole comments_count pokazujące ilość komentarzy. Nadal jednak działa lazy loading i same komentarze nie są ładowane. Załadujmy posty z komentarzami:

$postsWithComments = Post::with('comments')->get();   

Teraz połączmy jedno z drugim – posty, komentarze + ilość komentarzy:

$postsWithCommentsAndCount = Post::with('comments')->withCount('comments')->get();    

Teraz załadujmy tylko te posty, które mają jakiś komentarz:

$commentedPosts = Post::has('comments')->get();   

Ta komenda ładuje posty, które mają komentarz. Nadal jednak samych komentarzy nie ładuje, ale tutaj sobie też poradzimy:

$commentedPostsWithComments = Post::has('comments')->with('comments')->get();  

Załadujmy sobie post o ID 15:

$post15 = Post::find(15); 

Teraz z załadowanymi komentarzami:

$post15 = Post::find(15)->load('comments'); 
$post15->comments; 

Post ID 15 z ilością komentarzy:

$post15 = Post::withCount('comments')->find(15);  

Post ID 15 z komentarzami załadowanymi, ale nie przez load:

$post15 = Post::with('comments')->find(15);  

Laravelowego Eloquent ORMa trzeba po prostu wyczuć. Często pierwsza komenda to metoda statyczna, a potem chainujemy normalne metody. Czasami kończymy metodą get, czasami nie. Proszę zwrócić uwagę na przykład poniżej:

$post15 = Post::with('comments')->withCount('comments')->find(15);   

Ładujemy post ID 15 z komentarzami i ilością komentarzy. Nie ma żadnej specjalnej reguły tutaj, że with jest statyczna a reszta nie, with jest statyczna, bo jest pierwsza, tworzy jakiś tam obiekt query z modelu Post, na którym to query chainujemy np. withCount a następnie natrafiamy na jakiś wyzwalacz, który query na szukany obiekt zamienia (tutaj jest to find, ale w przypadku kolekcji a nie rekordów po ID branych często jest to get)

Możemy ten sam efekt osiągnąć odwrotnie:

$post15 = Post::withCount('comments')->with('comments')->find(15); 

Ładujemy to samo, ale teraz withCount jest przed with i to ono jest statyczne. Podobny mechanizm – klasa modelu, metoda statyczna zamieniająca na jakąś klasę query, metoda query chainowana, wyzwalacz zwracający rekord/rekordy.

Da się to zrozumieć, choć na początku może nas to przytłaczać.

Local query scopes – własne elementy query

Przechodzimy do PostFactory, zmienić treść posta:

class PostFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'author' => fake()->name(),
            // 'post' => 'Lorem ipsum bla bla bla'
            'post' => 'inny post'
        ];
    }
}

Teraz treść to będzie inny post. Okej, teraz zmieniamy seeder:

class PostSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        // Post::factory()->count(10)->create()->each(function($post){
        //     $random_num = random_int(1,5);
        //     PostComment::factory()
        //     ->count($random_num)
        //     ->for($post)
        //     ->create();
        // });
        Post::factory()->count(2)->create();
    }
}

Dwa posty, zero komentarzy, teraz z 'innym postem’ jako treść. Będziemy tego potrzebowali, aby zaobserwować local query scopes w praktyce. Teraz musimy stworzyć te nasze dwa posty. Jeżeli jeszcze jesteśmy w tinkerze to wychodzimy (ctrl + c) i używamy komendy:

php artisan db:seed --class=PostSeeder

Możemy sprawdzić, czy coś się zmieniło naszą własną komendą (z console.php):

php artisan postcount

Teraz pora odwiedzić Models/Post.php i utworzyć local query scope:

use Illuminate\Database\Eloquent\Builder;
class Post extends Model
{
    use HasFactory;
    protected $fillable = ['author', 'post'];
    public function comments()
    {
        return $this->hasMany(PostComment::class);
    }
    public function scopeContent(Builder $query, string $content): Builder
    {
        return $query->where('post', 'LIKE', '%' . $content . '%');
    }
}

Teraz przechodzimy do tinkera:

php artisan tinker

Importujemy namespace:

use App\Models\Post;   

A teraz magia:

$inne = Post::content('inny')->get();    

Dostajemy nasze dwa posty. Przeanalizujmy raz jeszcze nasz scope. Funkcja musi zaczynać się od scope i wielkiej litery, która w query będzie małą literą i bez scope. Ma mieć wstrzyknięty builder oraz może przyjąć argument, zwrócić ma builder.

Zwracamy tam query, do którego chainujemy do, co chcemy osiągnąć i podajemy dalej. Dalej można użyć wyzwalacza (np. get) który zamienia query na rekord/rekordy (swoją drogą wyzwalacz to jest moja własna nazwa, ja tak to nazywam, nie żadna oficjalna terminologia), albo chainować kolejne query.

To chainujemy – teraz spróbujmy załadować ilość komentarzy (będzie 0 dla obu postów bo tak zmieniliśmy nasz seeder):

$inne = Post::content('inny')->withCount('comments')->get();   

To teraz ilość postów zawierających słowo inny:

$inne = Post::content('inny')->count();

Możemy się tak bawić w nieskończoność.

Local scope popular – najbardziej popularne posty

Piszemy własny scope dla najbardziej popularnych postów – za popularność będziemy uznawać największą ilość komentarzy dołączonych do danego posta. Najpierw przećwiczymy to sobie w tinkerze, do którego przejdziemy komendą:

php artisan tinker

Tradycyjnie importujemy namespace modelu Post:

use App\Models\Post;   

Teraz spróbujemy wyświetlić wszystkie posty w kolejności od największego ID malejąco:

 Post::orderBy('id', 'desc')->get();   

Teraz zróbmy to samo, ale z ilością komentarzy. Ilość komentarzy nie jest domyślnie dostępna, ale wiemy przecież, że możemy tę ilość zaciągnąć poprzez metodę withCount:

Post::withCount('comments')->get();    

Zaciągnięta ilość postów występuje pod kolumną o nazwie „comments_count”. Zresztą, query możemy sobie wyświetlić przy pomocy metody toSql():

Post::withCount('comments')->toSql(); 
//"select `posts`.*, (select count(*) from `post_comments` where `posts`.`id` = `post_comments`.`post_id`) as `comments_count` from `posts`"

Może to okazać się czasami pomocne. Metodę toSql() możemy wykonać na klasie query, ale na kolekcji (czyli np. po metodzie get) czy innej wartości zamieniającej query na jakiś typ zwracany (np. count()) – już nie.

Czasami może okazać się przydatne zobaczenie jak to query, które napisaliśmy, wyglądałoby w czystym SQL. Okej.

Mamy kolumnę 'comments_count’, możemy zatem spróbować użyć orderBy z tą kolumną:

Post::withCount('comments')->orderBy('comments_count', 'desc')->get();   

W SQL wyglądałoby to tak:

Post::withCount('comments')->orderBy('comments_count', 'desc')->toSql();
//"select `posts`.*, (select count(*) from `post_comments` where `posts`.`id` = `post_comments`.`post_id`) as `comments_count` 
//from `posts` 
//order by `comments_count` desc"

Mamy już wszystko, czego potrzebujemy, do napisania scopePopular. Wychodzimy z tinkera (ctrl+c) i przechodzimy do naszego modelu Post w już dobrze znanym nam namespace.

Funkcja scope musi zaczynać się od słowa 'scope’, potem wielką literą słowo, które w query użyte zostanie małą, potem minimum jeden argument – Builder, typ zwracany Builder, zwracamy query.

Resztę mamy opanowaną, więc możemy napisać już naszą funkcję:

public function scopePopular(Builder $query) : Builder 
    {
        return $query->withCount('comments')
        ->orderBy('comments_count', 'desc');
    }

Teraz możemy raz jeszcze wejść do tinkera i sprawdzić, czy chodzi:

php artisan tinker
use App\Models\Post;
Post::popular()->get();
Post::popular()->take(5)->get();
Post::popular()->with('comments')->get();
Post::popular()->take(1)->with('comments')->get();    

Powinniśmy już coraz lepiej czuć się w budowaniu queries z Laravelowym ORMem. Jeżeli nie – na każdej z tych komend możemy zamiast get wywołać toSql i sprawdzić jak dokładnie działają.

Piszemy kolejną komendę – używamy having w naszym query

Wcześniej w pliku console.php (obok web.php) napisaliśmy własną komendę artisana, która sprawdzała, ile jest postów z komentarzami i wyświetlała to nam w konsoli. Tak wygląda nasza komenda:

Artisan::command('postswherecomments', function () {
    $cnt = Post::has('comments')->count();
    $this->comment("There are $cnt posts with comments");
})->purpose('Shows n/o posts with comments');

Napiszemy ją jeszcze raz, tym razem korzystając z withCount oraz having:

Artisan::command('commented', function () {
    $cnt = Post::withCount('comments')
    ->having('comments_count', '>', 0)
    ->count();
    $this->comment("There are $cnt posts with comments");
})->purpose('Shows n/o posts with comments');

Teoretycznie niewiele się zmienia, w praktyce having jest dużo potężniejsze i możemy teraz podliczyć posty o dowolnej ilości komentarzy, nie tylko takie, które posiadają jakikolwiek.

Możemy zatem napisać bardziej zaawansowaną komendę:

Artisan::command('comments_over {number}', function (int $number) {
    $cnt = Post::withCount('comments')
    ->having('comments_count', '>', $number)
    ->count();
    $this->comment(" $cnt posts with more than $number comments");
})->purpose('Shows n/o posts with comments');

Teraz mamy kontrolę powyżej ilu postów chcemy te posty podliczyć. Możemy to wypróbować w terminalu. W naszym modelu również możemy pisać scope używając having, na przykład:

public function scopeCommented(Builder $query) : Builder 
{
   return $query->withCount('comments')->having('comments_count', '>', 0);
}

Teraz w tinkerze możemy (po zaimportowaniu namespace) wypróbować nasz scope:

use App\Models\Post;  
Post::commented()->get(); //wyświetla posty, ale tylko te z komentarzami
Post::commented()->count(); //ilość postów z komentarzami
Post::commented()->popular()->get(); // używa dwóch naszych local scopes - posty z komentarzami od największej ilości komentarzy w dół

Bardziej zaawansowany seeding – factory definitions

Local query scopes to fajne narzędzie, dzięki któremu można m. in. zaimplementować możliwość wyszukiwania i filtrowania rekordów (ale nie tylko). Warto w tym momencie pochylić się nieco nad naszymi fabrykami i seederami i odrobinę je ulepszyć.

Po pierwsze, chcemy aby PostCommentFactory zwracało losowe zdanie dla naszego komentarza, niech ma średnio 3 wyrazy:

class PostCommentFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            //'comment' => 'lorem ipsum comment bla bla bla'
            'comment' => fake()->sentence(3), 
        ];
    }
}

Po drugie, chcemy aby PostFactory tworzył rekordy utworzone w jakimś interwale czasowym (przyda się do query scopes i filtrowania). Niech będzie, że created_at (dotychczas ustawiane domyślnie na czas utworzenia) to losowy przedział od dwóch lat wstecz do teraz, zaś updated_at to losowa data od created_at do teraz:

class PostFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'author' => fake()->name(),
            // 'post' => 'Lorem ipsum bla bla bla'
            //'post' => 'inny post'
            'post' => fake()->sentence(3),
            'created_at' => fake()->dateTimeBetween('-2 years'),
            'updated_at' => function (array $attributes) {
                return fake()->dateTimeBetween($attributes['created_at'], 'now');
            },
        ];
    }
}

Teraz tylko upewnijmy się, że nasz PostSeeder odpowiednio tworzy posty i komentarze z relacjami:

class PostSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Post::factory()->count(10)->create()->each(function($post){
            $random_num = random_int(1,5);
            PostComment::factory()
            ->count($random_num)
            ->for($post)
            ->create();
        });
        //Post::factory()->count(2)->create();
    }
}

Możemy teraz utworzyć nowe, fejkowe rekordy przy pomocy komendy:

php artisan db:seed --class=PostSeeder

Tworzenie modeli i ich asocjacja – piszemy komendę

Na razie tworzyliśmy relacje pomiędzy modelami za pomocą seederów i fabryk. Nie będziemy teraz tworzyć widoków, formularzy i kontrolerów, aby pokazać jak działa asocjacja modelów i ich relacji, ale wypróbujemy to sobie w inny sposób – pisząc własną komendę w pliku console.php.

Nasza komenda zapyta nas o autora, treść posta oraz treść komentarza, wyświetli co wpisaliśmy i zapyta, czy chcemy kontynuować. Bez zbędnego wydłużania – tak to się robi:

Artisan::command('create_post', function () {
    $author = $this->ask('Enter author name: ');
    $text = $this->ask('Enter post text: ');
    $comment = $this->ask('Enter comment: ');
    $this->comment("Author: $author");
    $this->comment("Text: $text");
    $this->comment("Comment: $comment");
    if ($this->confirm('Do you wish to continue?', true)) {
        $this->comment("Lets create our post and comment");
    }
});

Teraz możemy to wypróbować poprzez php artisan create_post. Okej, część związaną z obsługą konsoli mamy już za sobą, teraz pora utworzyć model Post, PostComment, i odpowiednio zapisać tak, aby doszło do asocjacji między dwoma modelami.

Po pierwsze, musimy się upewnić, że mamy odpowiednie importy na samej górze pliku zawierające namespace modelów Post i PostComment:

use App\Models\Post;
use App\Models\PostComment;

Po drugie – tworzymy jeden i drugi model i połączenie pomiędzy nimi:

Artisan::command('create_post', function () {
    $author = $this->ask('Enter author name: ');
    $text = $this->ask('Enter post text: ');
    $comment = $this->ask('Enter comment: ');
    $this->comment("Author: $author");
    $this->comment("Text: $text");
    $this->comment("Comment: $comment");
    if ($this->confirm('Do you wish to continue?', true)) {
        $this->comment("Creating post and comment");
        $post = Post::create(['author' => $author, 'post' => $text]);
        $postcomment = new  PostComment();
        $postcomment->comment = $comment;
        $post->comments()->save($postcomment);
        $this->comment("Done");
    }
});

Relacja jest następująca:

  • post i komentarz mają relację one-to-many
  • post posiada wiele komentarzy – metoda comments modelu Post zwraca hasMany(PostComment::class)
  • komentarz posiada jeden post – metoda post modelu PostComment zwraca belongsTo(Post::class)

W związku z tym na naszym wpisie możemy wykonać metodę comments a na niej metodę save przekazując do niej utworzony i niezapisany komentarz, który zostanie połączony z postem i zapisany.

Możemy też całość wykonać od innej strony:

Artisan::command('create_post', function () {
    $author = $this->ask('Enter author name: ');
    $text = $this->ask('Enter post text: ');
    $comment = $this->ask('Enter comment: ');
    $this->comment("Author: $author");
    $this->comment("Text: $text");
    $this->comment("Comment: $comment");
    if ($this->confirm('Do you wish to continue?', true)) {
        $this->comment("Creating post and comment");
        $post = Post::create(['author' => $author, 'post' => $text]);
        $postcomment = new  PostComment();
        $postcomment->comment = $comment;
        $postcomment->post()->associate($post)->save();
        $this->comment("Done");
    }
});

Komentarz posiada metodę post kierującą na jednego rodzica. Możemy na utworzonym (i niezapisanym) komentarzu wykonać metodę post, na niej associate i przekazać jej (utworzony i zapisany) post i wszystko zapisać.

Zasada jest prosta:

  • post posiada komentarze – komentarze zapisujemy do posta poprzez $post->comments()->save($komentarz)
  • komentarz należy do posta – dokonujemy asocjacji komentarza z postem poprzez $postcomment->post()->associate($post)->save()
  • w obu przypadkach mamy do czynienia z utworzonym i zapisanym postem oraz utworzonym ale niezapisanym komentarzem
  • $post->comments()->save($komentarz) zapisuje utworzony komentarz do posta, który ma komentarze
  • $postcomment->post()->associate($post)->save() dokonuje asocjacji utworzonego (i niezapisanego) komentarza z postem (utworzonym i zapisanym) następnie zapisuje komentarz z już ustaloną asocjacją z postem

Wszystko dzieje się tak, ponieważ możemy utworzyć i zapisać post, ale nie możemy utworzyć i zapisać komentarza i później to wszystko połączyć – bo komentarz ma w bazie danych pole 'post_id’, (bo on należy do posta) i to pole nie może być puste.

Czyli:

  • tworzymy i zapisujemy posta
  • tworzymy bez zapisywania komentarz
  • wybieramy podejście
    • albo do posta zapisujemy komentarz – $post->comments()->save($komentarz)
    • albo dokonujemy asocjacji utworzonego komentarza do posta i następnie go zapisujemy – $postcomment->post()->associate($post)->save()

Zapisywanie do, zapisywanie wielu do i asocjacja – raz jeszcze

Rzućmy okiem na nasz model Post:

class Post extends Model
{
    use HasFactory;
    protected $fillable = ['author', 'post'];
    public function comments()
    {
        return $this->hasMany(PostComment::class);
    }
    (...)
}

Relacja hasMany względem komentarza w metodzie comments oznacza, że:

  • Post ma wiele komentarzy, dostępne pod metodą $post->comments()
  • Do zapisanego posta można utworzone i niezapisane komentarze dodawać i zapisywać metodą $post->comments()->save($comment)

Teraz rzućmy okiem na model PostComment:

class PostComment extends Model
{
    use HasFactory;
    protected $fillable = ['comment'];
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

Relacja belongsTo komentarza względem postu w metodzie post oznacza, że:

  • komentarz może być przypisany tylko do jednego posta
  • do już istniejącego (zapisanego) posta możemy na utworzonym (niezapisanym) komentarzu dokonać asocjacji z postem i zapisania poprzez $comment->post()->associate($post)->save() – tutaj post jest metodą modelu PostComment, zaś $post to zmienna zawierająca już istniejący post, do którego dokonujemy asocjacji

To zapisywanie modelu należącego do innego modelu może być mylące, nikt nie twierdzi, że wszystko jest łatwe i oczywiste do zrozumienia, ale tak to działa. Asocjacja to przypisanie modelu należącego do innego modelu. Komentarz jest modelem-dzieckiem, Post modelem-rodzicem.

Wydaje się więc naturalne, że aby zapisać komentarz:

  • zapisany post musi istnieć
  • należy utworzyć komentarz i przypisać mu treść
  • zanim zapiszemy komentarz dokonać jakiegoś połączenia z postem
  • gdy mamy komentarz z treścią i połączeniem z postem-rodzicem, zapisujemy komentarz.

Od strony modelu rodzica sprawy wydają się łatwiejsze – do istniejącego posta zapisujemy komentarz. Zwróćmy jednak uwagę, że o ile komentarz ze swoją relacją belongsTo nie ma pojęcia, czy jest w relacji jeden do jednego czy jeden do wielu (on po prostu należy do posta) o tyle post za pomocą hasMany ma wiedzę, że znajduje się w relacji jeden do wielu.

Gdyby sytuacja była inna, mamy model-rodzic Profil, który posiada relację hasOne do modelu-dziecka adres, dziecko zaś relację belongsTo do profilu, sprawy byłyby jasne – profil może zapisywać do siebie adres, od drugiej strony to adres może dokonywać asocjacji ze swoim profilem, wybieramy co chcemy.

Tutaj jednak jest relacja jeden do wielu, jeden post może mieć wiele komentarzy. Może też chcieć zapisać wiele komentarzy na raz, zamiast robić to krok po kroku metodą save.

Od tego jest metoda saveMany i takie wykorzystanie tej metody sobie napiszemy w console.php:

Artisan::command('create_post_save_many', function () {
    $author = $this->ask('Enter author name: ');
    $text = $this->ask('Enter post text: ');
    $this->comment("Author: $author");
    $this->comment("Text: $text");
    if ($this->confirm('Do you wish to continue?', true)) {
        $this->comment("Creating post and comment");
        $post = Post::create(['author' => $author, 'post' => $text]);
        $comments = array(
            new PostComment(['comment' => 'comment 1']),
            new PostComment(['comment' => 'comment 2']),
            new PostComment(['comment' => 'comment 3']),
        );
        $post->comments()->saveMany($comments);
    }
});

Ta komenda tworzy posta o podanym autorze i podanej treści, następnie zapisuje do posta 3 utworzone w jednym rzucie komentarze za pomocą saveMany. Gdyby relacja była jeden do jednego a nie jeden do wielu, nie moglibyśmy tego używać.