Bliżej i dokładniej poznajemy najważniejsze relacje Laravela – jeden do jednego, jeden do wielu oraz równorzędna relacja wiele do wielu. Po tym artykule relacje niepolimorficzne nie będą miały dla nas tajemnic. Jednocześnie budujemy podwalinę pod zrozumienie polimorficznych odpowiedników tych relacji.

Relacja jeden do jednego – profil i adres użytkownika

Tworzymy model, migrację, fabrykę i seeder dla profilu użytkownika znaną nam już komendą Laravela:

php artisan make:model Profile -mfs

Przechodzimy do migracji i tworzymy pole typu string metodą string (max 255 znaków) dla imienia użytkownika:

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

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

Teraz możemy migrować zmiany:

php artisan migrate

Następnie tworzymy model, migrację i fabrykę dla adresu, który każdy profil będzie posiadać w relacji jeden do jednego (czyli jeden profil ma jeden adres i odwrotnie):

php artisan make:model Address -mf 

Model Profile będzie posiadał jeden adres (hasOne) zaś jeden adres będzie należał do profilu (belongsTo). Zatem to w migracji profilu, poza polami miasto i ulica, musimy dodać klucz obcy będący referencją do profilu:

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('addresses', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('city')->nullable(false);
            $table->string('street')->nullable(false);
            $table->foreignId('profile_id')->constrained()
                ->cascadeOnDelete();
        });
    }

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

Teraz możemy migrować. Następnie przechodzimy do modelu Profilu, zaimportować model Adresu, dodać relację hasOne oraz wypełnić fillable:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Address;

class Profile extends Model
{
    use HasFactory;
    protected $fillable = ['name'];
    function address() {
        return $this->hasOne(Address::class);
    }
}

Teraz kończymy naszą relację po drugiej stronie, dodając do modelu adresu referencję do modelu profilu plus wypełniając fillable:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\Profile;
class Address extends Model
{
    use HasFactory;
    protected $fillable = ['city', 'street'];
    public function profile() {
        return $this->belongsTo(Profile::class);
    }
}

Relacja jeden do jednego utworzona i gotowa do używania.

Tworzymy fabryki i seedery dla naszej relacji profil-adres

Przechodzimy do fabryki dla modelu Profil i tworzymy definicję, jak tworzyć sztuczne dane – jak zwykle użyjemy fakera:

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

Podobnie będzie w fabryce dla modelu Adress, również tworzymy definicję i korzystamy z fakera:

class AddressFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'city' => fake()->city(),
            'street' => fake()->streetAddress()
        ];
    }
}

Teraz nie pozostaje nam nic innego, jak udać się do seedera dla modelu Profil i wykonać tam seeding profilów i powiązanych z nimi adresów:

(...)
use App\Models\Profile;
use App\Models\Address;
class ProfileSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Profile::factory()->count(10)->create()->each(function($profile){
            Address::factory()
            ->count(1)
            ->for($profile)
            ->create();
        });
    }
}

Możemy teraz odpalić naszego seedera przy pomocy komendy Laravela, podając nazwę klasy seedera:

php artisan db:seed --class=ProfileSeeder

W bazie danych mamy już teraz 10 profilów i 10 adresów, odpowiednio ze sobą powiązanych. Można pracować dalej.

Podstawowe operacje relacji jeden do jednego – komendy

Będziemy tworzyć własne komendy artisana (plik console.php) aby nie zamęczać się zbytnio pobocznymi tematami. Zaczniemy od importu modelu Profile i Address w tym pliku:

use App\Models\Profile;
use App\Models\Address;

Piszemy dwie komendy, pokazujące nam ilość profilów oraz ilość adresów. Nie powinno być to dla nas szczególnie trudne. Oto kod:

Artisan::command('profiles', function () {
    $cnt = Profile::count();
    $this->comment("There are $cnt profiles in database");
});
Artisan::command('addresses', function () {
    $cnt = Address::count();
    $this->comment("There are $cnt addresses in database");
});

Nasze komendy możemy wywołać jedna po drugiej:

php artisan profiles; php artisan addresses;

Piszemy komendy pokazujące ile jest profilów posiadających adres, oraz takich, które go nie posiadają (bierzemy ilość wszystkich i odejmujemy te z adresem):

Artisan::command('hasaddr', function () {
    $cnt = Profile::has('address')->count();
    $this->comment("There are $cnt profiles with address");
});
Artisan::command('hasnoaddr', function () {
    $all = Profile::count();
    $withaddr = Profile::has('address')->count();
    $noaddr = $all - $withaddr;
    $this->comment("There are $noaddr profiles with address");
});

Tworzymy takie same komendy, ale dla adresów – sprawdzające ile jest adresów z profilem i bez:

Artisan::command('hasprofile', function () {
    $cnt = Address::has('profile')->count();
    $this->comment("There are $cnt addresses with profile");
});
Artisan::command('hasnoprofile', function () {
    $all = Address::count();
    $withprofile = Address::has('profile')->count();
    $noprofile = $all - $withprofile;
    $this->comment("There are $noprofile addresses without profile");
});

Ostatnią rzeczą, jaką w tej sekcji zrobimy, będzie komenda pozwalająca utworzyć profil (ale bez adresu) z poziomu artisana:

Artisan::command('create_profile', function () {
    $name = $this->ask('Enter profile name: ');
    $this->comment("Profle name: $name");
    if ($this->confirm('Do you wish to continue?', true)) {
        Profile::create(['name' => $name]);
        $this->comment("Profile created");
    }
});

Możemy jeszcze stworzyć alternatywną wersję komendy, która pozwala wpisać imię z palca na samym początku:

Artisan::command('create_profile2 {name}', function (string $name) {
    $this->comment("Profle name: $name");
    if ($this->confirm('Do you wish to continue?', true)) {
        Profile::create(['name' => $name]);
        $this->comment("Profile created");
    }
});

W takim wypadku musimy pamiętać, aby name przekazać w cudzysłowie (no chyba, że to jeden wyraz):

php artisan create_profile2 'John Doe'

Zapisywanie adresu do profilu – piszemy komendę

Mamy do czynienia z relacją jeden do jednego, w której to model Profil jest rodzicem, zaś model Address dzieckiem. Świadczy o tym ten fragment z modelu Profil:

function address() {
        return $this->hasOne(Address::class);
}

Profil posiada adres. Zatem na metodzie profilu address można wykonywać metodę ->save(), która utworzy i zapisze adres połączony z tym profilem. Napiszmy to sobie:

Artisan::command('profile_addr', function () {
    $name = $this->ask('Enter profile name: ');
    $city = $this->ask('Enter city: ');
    $street = $this->ask('Enter street: ');
    $this->comment("Profle name: $name");
    $this->comment("Profle name: $city");
    $this->comment("Profle name: $street");
    if ($this->confirm('Do you wish to continue?', true)) {
        $profile = Profile::create(['name' => $name]);
        //profil utworzony i zapisany (w bazie danych)

        $addr = new Address(['city' => $city, 'street' => 'street']);
        //adres bez profilu utworzony (w pamięci komputera) i niezapisany

        $profile->address()->save($addr);
        //adres połączony z profilem i zapisany do niego

        $this->comment("Profile with address created");
    }
});

Zapisywanie adresu do profilu od drugiej strony – piszemy komendę

Mamy do czynienia z relacją jeden do jednego albo relacją jeden do wielu – patrząc na model Address tego nie wiemy, jedyne co wiemy, to że Address jest dzieckiem w relacji do modelu Profil, o czym świadczy poniższa linijka:

public function profile() {
        return $this->belongsTo(Profile::class);
    }

Adres należy do profilu, jest więc w tej relacji dzieckiem. Oznacza to, że na metodzie profile modelu Address możemy wykonać metodę ->associate(), która podłączy utworzony (w pamięci komputera) adres z utworzonym i zapisanym (w bazie danych) profilem. Po tym trzeba będzie wykonać metodę ->save(), która adres wpisze do bazy danych.

Możemy zatem przerobić poprzednią metodę, aby korzystała z associate i wykonywała to samo, ale od drugiej strony:

Artisan::command('addr_profile', function () {
    $name = $this->ask('Enter profile name: ');
    $city = $this->ask('Enter city: ');
    $street = $this->ask('Enter street: ');
    $this->comment("Profle name: $name");
    $this->comment("Profle name: $city");
    $this->comment("Profle name: $street");
    if ($this->confirm('Do you wish to continue?', true)) {
        $profile = Profile::create(['name' => $name]);
        //profil utworzony i zapisany (w bazie danych)

        $addr = new Address(['city' => $city, 'street' => 'street']);
        //adres bez profilu utworzony (w pamięci komputera) i niezapisany

        $addr->profile()->associate($profile)->save();
        //asocjacja adresu w pamięci z profilem w bazie danych
        //zapisanie adresu połączonego z profilem do bazy danych

        $this->comment("Profile with address created");
    }
});

Jeżeli nie podoba nam się associate, nie musimy z niego korzystać. Bywają jednak takie przypadki, że jesteśmy zmuszeni. Przypomnijmy sobie naszą komendę create_profile – tworzy ona i zapisuje do bazy danych profil, ale bez adresu.

Zmodyfikujemy sobie tę komendę, aby logowała nam ID tego utworzonego profilu (bez adresu – przyda się za chwilę):

Artisan::command('create_profile3 {name}', function (string $name) {
    $this->comment("Profle name: $name");
    if ($this->confirm('Do you wish to continue?', true)) {
        $profile = Profile::create(['name' => $name]);
        $this->comment("Profile created. ID:{$profile->id}");
    }
});

Teraz utworzymy sobie profil o nazwie 'Jane Doe’ i zobaczymy od razu, jaki ID ma nasz profil bez adresu:

php artisan create_profile3 'Jane Doe'

U mnie ID to 16. Teraz napiszmy sobie komendę, która przyjmie ID i pozwoli utworzyć nam adres dla już istniejącego profilu i go ładnie połączy. Na początek sprawdźmy, czy dobrze wyszukuje ID:

Artisan::command('addrassoc {id}', function (int $id) {
    $profile = Profile::find($id);
    $city = $this->ask('Enter city: ');
    $street = $this->ask('Enter street: ');
    $this->comment("Profle name: {$profile->name}");
    $this->comment("Profle name: $city");
    $this->comment("Profle name: $street");
    if ($this->confirm('Do you wish to continue?', true)) {
        $this->comment("Lets create and associate");
    }
});

Teraz po wpisaniu poprawnego ID (u mnie 16, ID profilu bez adresu o nazwi 'Jane doe’) powinno zapytać nas o miasto i ulicę i symulować tworzenie, po komendzie:

php artisan addrassoc 16

Jeżeli działa, to czas napisać asocjację:

Artisan::command('addrassoc {id}', function (int $id) {
    $profile = Profile::find($id);
    $city = $this->ask('Enter city: ');
    $street = $this->ask('Enter street: ');
    $this->comment("Profle name: {$profile->name}");
    $this->comment("Profle name: $city");
    $this->comment("Profle name: $street");
    if ($this->confirm('Do you wish to continue?', true)) {
        $addr = new Address(['city' => $city, 'street' => $street]);
        //adres utworzony w pamięci, niezapisany, bez połączenia z profilem

        $addr->profile()->associate($profile)->save();
        //adres połączony z profilem w bazie danych
        //adres zapisany do bazy danych

        $this->comment("Address associated with profile with ID {$id}");
    }
});

Relacje jeden do wielu – drużyna, gracz

Tworzymy relację jeden do wielu. Zaczniemy od utworzenia modelu Team, razem z migracją, fabryką i seederem przy pomocy komendy Laravela:

php artisan make:model Team -mfs

Przechodzimy do migracji utworzonej razem z modelem Team i dodajemy jedno pole, jakim jest nazwa drużyny:

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

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

Migrujemy zmiany korzystając z komendy:

php artisan migrate

Tabela dla drużyn już w SQL utworzona. Pora utworzyć model Player wraz z migracją i fabryką, przy pomocy komendy:

php artisan make:model Player -mf

Przechodzimy do nowo utworzonej migracji dla tabeli z naszymi graczami i dodajemy pola wiek oraz imię oraz referencję do modelu-rodzica:

return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('players', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->string('name');
            $table->integer('age');
            $table->foreignId('team_id')->constrained()
                ->cascadeOnDelete();
        });
    }

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

Migrujemy raz jeszcze:

php artisan migrate

Idziemy do modelu-rodzica, czyli Team, zaimportować model-dziecko, wypełnić fillable oraz zaznaczyć relację jeden do wielu, w której to drużyna posiada wielu graczy:

(...)
use App\Models\Player;

class Team extends Model
{
    use HasFactory;
    protected $fillable = ['name'];
    public function players() {
        return $this->hasMany(Player::class);
    }
}

Udajemy się do modelu gracza czyli modelu-dziecka zaznaczyć drugi koniec tej relacji:

(...)
use App\Models\Team;

class Player extends Model
{
    use HasFactory;
    protected $fillable = ['name', 'age'];
    public function team(){
        return $this->belongsTo(Team::class);
    }
}

Jesteśmy gotowi do pracy.

Tworzymy fabryki i seedery dla relacji drużyna-gracz

Idziemy do TeamFactory, ustalić definicje jak tworzyć fejkowe drużyny:

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

Teraz PlayerFactory, czyli fabryka dla fejkowych graczy – ustalamy definicje:

class PlayerFactory 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)
        ];
    }
}

Teraz TeamSeeder, gdzie stworzymy drużyny oraz losowych graczy w losowej ilości:

(...)
use App\Models\Team;
use App\Models\Player;
class TeamSeeder extends Seeder
{
    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        Team::factory()
        ->count(5)
        ->create()
        ->each(function($team){
            Player::factory()
            ->count(random_int(3,6))
            ->for($team)
            ->create();
        });
    }
}

Po wszystkim należy wywołać powyższy seeder przy pomocy wielokrotnie już używanej komendy Laravela, podając nazwę klasy seedera:

php artisan db:seed --class=TeamSeeder

Fejkowe drużyny i graczy już mamy w bazie danych.

Podstawowe operacje relacji jeden do wielu – piszemy komendy

W pliku console.php importujemy modele Team oraz Player:

use App\Models\Team;
use App\Models\Player;

Piszemy komendę wyświetlającą info o wybranej drużynie z przekazanym id:

Artisan::command('teaminfo {id}', function (int $id) {
    $team = Team::withCount('players')->find($id);
    $this->comment("Team name: {$team->name}");
    $this->comment("Number of players: {$team->players_count}");
});

Piszemy komendę wyświetlającą info o wybranym graczu z przekazanym id:

Artisan::command('playerinfo {id}', function (int $id) {
    $player = Player::with('team')->find($id);
    $this->comment("Player name: {$player->name}");
    $this->comment("Player age: {$player->age}");
    $this->comment("Team name: {$player->team->name}");
});

Teraz coś ciekawego – piszemy komendę, która pozwala nam złapać (po ID) gracza i przesunąć go do innej drużyny (po ID):

Artisan::command('player_change_team {id_p} {id_t}', function (int $id_p, int $id_t) {
    $player = Player::find($id_p);
    $new_team = Team::find($id_t);
    $player->team()->dissociate();
    $player->team()->associate($new_team);
    $player->save();
});

W moim przypadku przesuwałem gracza o ID 1 do drużyny o ID 2. Na początku i na końcu wylogowałem sobie jego informacje:

php artisan playerinfo 1
php artisan player_change_team 1 2
php artisan playerinfo 1

Gracz został przeniesiony.

Zapisywanie gracza/graczy do drużyny – relacja jeden do wielu

Mamy do czynienia z relacją jeden do wielu, w której to drużyna jest modelem-rodzicem, zaś gracz modelem-dzieckiem, o czym świadczy ten fragment w modelu Team:

 public function players() {
        return $this->hasMany(Player::class);
    }

Drużyna ma wielu graczy. Do drużyny można zapisywać gracza metodą ->players()->save() oraz wielu graczy na raz metodą ->players()->saveMany().

Utworzymy sobie komendę, która tworzy drużynę, gracza i zapisuje. Powtarzamy coś, co już wcześniej robiliśmy:

Artisan::command('team_player', function () {
    $tname = $this->ask('Enter team name: ');
    $pname = $this->ask('Enter player name: ');
    $age = $this->ask('Enter player age: ');
    $this->comment("Team name: $tname");
    $this->comment("Player name: $pname");
    $this->comment("Player age: $age");
    if ($this->confirm('Do you wish to continue?', true)) {
        $team = Team::create(['name' => $tname]);
        //team utworzony i zapisany (w bazie danych)

        $player = new Player(['name' => $pname, 'age' => (int)$age]);
        //player bez drużyny utworzony (w pamięci komputera) i niezapisany

        $team->players()->save($player);
        //player połączony z teamem i zapisany do niego

        $this->comment("Team with player created");
    }
});

Możemy sprawdzić w bazie danych, czy ta komenda poprawnie dodaje drużynę i gracza dla tej drużyny. Działa.

Korzystając z tego, że mamy do czynienia z relacją jeden do wielu zróbmy jeszcze komendę, która utworzy drużynę i przez saveMany zapisze tam kilku graczy:

Artisan::command('team_players', function () {
    $tname = $this->ask('Enter team name: ');
    $this->comment("Team name: $tname");
    if ($this->confirm('Do you wish to continue?', true)) {
        $team = Team::create(['name' => $tname]);
        $players = array(
            new Player(['name' => 'Player1', 'age' => 18]),
            new Player(['name' => 'Player2', 'age' => 18]),
            new Player(['name' => 'Player3', 'age' => 18]),
            new Player(['name' => 'Player4', 'age' => 18]),
        );
        $team->players()->saveMany($players);
        
        $this->comment("Team with players created");
    }
});

Relacja jeden do wielu – używanie having

Przypomnijmy sobie taką komendę, którą napisaliśmy tworząc poprzednią relację (jeden do jednego):

Artisan::command('hasaddr', function () {
    $cnt = Profile::has('address')->count();
    $this->comment("There are $cnt profiles with address");
});

To samo możemy napisać, aby mieć komendę pokazującą ilu jest graczy przypisanych do jakiejś drużyny:

Artisan::command('hasteam', function () {
    $cnt = Player::has('team')->count();
    $this->comment("There are $cnt players with a team");
});

I podobnie możemy uczynić z drużynami:

Artisan::command('hasplayers', function () {
    $cnt = Team::has('players')->count();
    $this->comment("There are $cnt teams with players");
});

Mamy do czynienia z relacją jeden do wielu, więc od strony modelu-rodzica Team możemy też użyć having:

Artisan::command('hasplayers2', function () {
    $cnt = Team::withCount('players')->having('players_count', '>', 0)->count();
    $this->comment("There are $cnt teams with players");
});

Przewaga having nad has jest taka, że możemy sprawdzać dowolny warunek. Napiszmy komendę, która wyświetli ile jest drużyn posiadających co najmniej 4 graczy:

Artisan::command('has4ormore', function () {
    $cnt = Team::withCount('players')->having('players_count', '>=', 4)->count();
    $this->comment("There are $cnt teams with at least 4 players");
});

Relacja równorzędna wiele do wielu – artykuł i tag

Relacja wiele do wielu jest relacją równorzędną. Nie ma modelu rodzica i modelu dziecka. Przykładem może być artykuł (który może mieć wiele tagów) i tag (który może być przypisany do wielu artykułów).

Napiszemy sobie taką relację. Tworzymy model Article wraz z fabryką, migracją i seederem:

php artisan make:model Article -mfs

Będziemy bardzo leniwi – artykuł ma tylko tytuł. W końcu skupiamy się tylko na relacjach. W migracji artykułu dodajemy:

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

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

Teraz pora migrować:

php artisan migrate

Tworzymy drugi koniec równorzędnej relacji wiele do wielu – model Tag wraz z migracją, fabryką i seederem:

php artisan make:model Tag -mfs

Teraz w migracji taga dodajemy tylko tagname – żadnych kluczy obcych, relacja jest równorzędna i wymaga osobnej tabeli piwotalnej, którą zaraz stworzymy. Najpierw migracja taga:

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

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

Teraz migrujemy i tworzymy tabelę piwotalną:

php artisan migrate
php artisan make:migration CreateArticleTag

Tabela piwotalna składa się z nazwy (liczba pojedyncza) pierwszego elementu i drugiego elementu, czyli CreateArticleTag. Uprzedzając – Article wcześniej, bowiem 'A’ występuje wcześniej w kolejności alfabetycznej.

Korzystanie z konwencji nazewniczych Laravela ułatwi nam sporo roboty, więc się trzymajmy tego. Przechodzimy do migracji tabeli piwotalnej i tam dodajemy klucze obce:

(...)
return new class extends Migration
{
    /**
     * Run the migrations.
     */
    public function up(): void
    {
        Schema::create('article_tag', function (Blueprint $table) {
            $table->id();
            $table->timestamps();
            $table->unsignedBigInteger('article_id');
            $table->unsignedBigInteger('tag_id');
            $table->foreign('article_id')->references('id')->on('articles');
            $table->foreign('tag_id')->references('id')->on('tags');
        });
    }

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

Teraz migrujemy:

php artisan migrate

W modelu Article importujemy Tag, wypełniamy fillable i zaznaczamy jeden koniec równorzędnej relacji wiele-do-wielu:

use App\Models\Tag;

class Article extends Model
{
    use HasFactory;
    protected $fillable = ['title'];
    public function tags(){
        return $this->belongsToMany(Tag::class);
    }
}

W modelu Tag importujemy Article, wypełniamy fillable i zaznaczamy drugi koniec równorzędnej relacji wiele-do-wielu:

use App\Models\Article;
class Tag extends Model
{
    use HasFactory;
    protected $fillable = ['tagname'];
    public function articles() {
        return $this->belongsToMany(Article::class);
    }
}

Jesteśmy gotowi do pracy.

Tworzymy fabryki i seedery – relacja równorzędna wiele do wielu

Przechodzimy do TagFactory napisać definicję tworzenia fejkowych tagów. Użyjemy fakera i zwykłego PHP:

class TagFactory extends Factory
{
    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'tagname' => "#" . str_replace(" ", "", fake()->city()),
        ];
    }
}

Teraz pora odwiedzić TagSeeder, zaimportować model Tag, ustalić ilość tworzonych tagów za jedną komendą:

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

Pora wywołać seedera wykonując komendę db:seed z przekazaną nazwą klasy:

php artisan db:seed --class=TagSeeder 

Utworzył tagi i całkiem nieźle to wygląda. Jeden tag na przykład to '#WestAlvis’. Tak ma być.

Teraz przechodzimy do fabryki modelu Article, czyli ArticleFactory, i tam piszemy definicję tworzenia fejkowych artykułów:

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

Zdanie, średnio 6 wyrazów. Pora napisać seeder dla artykułów. Przechodzimy do ArticleSeeder i uzupełniamy (pamiętając o imporcie modelu):

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

Pora odpalić seeder:

php artisan db:seed --class=ArticleSeeder

W bazie danych mamy już fejkowe artykuły.

Relacja równorzędna wiele do wielu – łączenie ze sobą

Na początku napiszmy komendę, która sprawdzi ile jest artykułów posiadających jakiś tag. Po pierwsze, importy (w console.php):

use App\Models\Article;
use App\Models\Tag;

Po drugie – komenda:

Artisan::command('hastag', function () {
    $cnt = Article::has('tags')->count();
    $this->comment("There are $cnt articles with tags");
});

Na razie mamy zero. Napiszmy komendę, która sprawdzi ile mamy artykułów bez tagów:

Artisan::command('hasnotag', function () {
    $all = Article::all()->count();
    $cnt = Article::has('tags')->count();
    $notags = $all - $cnt;
    $this->comment("There are $notags articles without tags");
});

Na razie mamy 10, chyba że więcej razy użyliśmy seedera. Napiszmy sobie jeszcze funkcję, która wyświetla informacje o artykule o podanym ID:

Artisan::command('articleinfo {id}', function (int $id) {
    $article = Article::withCount('tags')->find($id);
    $this->comment("Article title: {$article->title}");
    $this->comment("Number of tags: {$article->tags_count}");
});

Okej. Teraz poznamy kilka sposobów na dodawanie relacji pomiędzy tagami i artykułami. Relacja jest równorzędna, więc nie ma znaczenia, od której strony zaczniemy, są natomiast inne komplikacje.

Napiszemy sobie taką komendę:

Artisan::command('attach1', function () {
    $article = Article::find(1);
    $article->tags()->attach(Tag::find(1)->id);
});

Pierwszy artykuł zostanie sparowany z pierwszym tagiem. Po wywołaniu, (tylko jeden raz) sprawdźmy info artykułu o ID 1. Teraz piszemy detach i prędko go używamy:

Artisan::command('detach1', function () {
    $article = Article::find(1);
    $article->tags()->detach(Tag::find(1)->id);
});

Używamy i sprawdzamy artykuł 1. Teraz powinien mieć na powrót 0 tagów – rozłączyliśmy je. Teraz dodamy aż dwa tagi:

Artisan::command('attach2', function () {
    $article = Article::find(1);
    $article->tags()->attach([1,2]);
});

Poprzedni tag (o ID 1) rozłączyliśmy, ale gdybyśmy tego nie zrobili, to teraz dodając tagi o ID 1 i 2, sprawilibyśmy, że połączenie z tagiem 1 występowałoby w bazie danych dwa razy. Zaraz sobie z tym poradzimy, ale na razie rozłączmy znowu, tym razem wszystkie tagi:

Artisan::command('detach2', function () {
    $article = Article::find(1);
    $article->tags()->detach();
});

Po wykonaniu detach2 i articleinfo (dla ID 1) powinniśmy znowu mieć 0 tagów. Okej, wiemy jak dodawać jeden, dodawać wiele, usuwać jeden i usuwać wszystkie. Wiemy też, że lubią się w relacjach wiele do wielu w tabelach piwotalnych rekordy dublować.

Jest na to sposób – metoda syncWithoutDetaching. Ona synchronizuje bez odpinania (jest też sync, nie warto o niej na razie tutaj wspominać). Napiszemy sobie taką synchronizację:

Artisan::command('syncwd1', function () {
    $article = Article::find(1);
    $article->tags()->syncWithoutDetaching([1,2,3,4,5]);
});

Te komendę możemy wywołać dowolną ilość razy. Mogliśmy też wcześniej dodać coś attachem – i rekordy się nie zdublują.

Oczywiście, jeżeli teraz poprzez attach dodamy id, które już nam syncWithoutDetaching dodał, to się zdublują.

Żeby się nie zdublowało a zarazem, żeby synchronizacja dokładała nowe tagi nie kasując poprzednich (ani nie pozwalając na dublowanie) – używamy syncWithoutDetaching().

Chcemy coś dodać do relacji wiele-do-wielu? Nie pozwalając na dublowanie ale też nie usuwając już dodanych elementów?

Używamy syncWithoutDetaching.