Będziemy zabierać się za dogłębne już poznanie wszystkich relacji Laravlea, nie tylko pobieżne. To samo tyczy się migracji i innych narzędzi frameworka. Do dzieła.

Ok, utworzymy sobie migrację i model Tag:

php artisan make:model Tag -mfs

Migracja będzie prosta:

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')->unique();
        });
    }

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

Pytanie – co robi unique?

  • Tworzy constraint o zasięgu kolumnowym dla kolumny tagname, nie będzie można utworzyć dwóch takich samych tagnames
  • Tworzy indeks, aby móc efektywnie porównywać za każdym razem jak coś insertujemy, czy rekord z taką kolumną tagname już nie istnieje

To jest migracja tworząca tabelę, a zatem w down wystarczy zrobić dropIfExists, choćbyśmy nie wiem ile rzeczy nadodawali.

Ok, pora na komendę:

php artisan migrate

Teraz jeszcze rzućmy okiem na model:

class Tag extends Model
{
    use HasFactory;

    protected $fillable = ['tagname'];
}

Ustawiamy fillable na tagname, aby móc robić mass assignment, mówiliśmy już o tym. Jakie jeszcze mamy pola?

  • id, bigint(20) unsigned auto_increment, utworzony metodą ->id
  • created_at i updated_at, utworzone metodą timestamps, jeżeli korzystamy z Eloquenta i nie zmienialiśmy im nazwy, to same się będą wypełniać

Ok, sprawdźmy jak to wszystko działa. Piszemy komendę insert tag z fasadą DB i korzystamy z niej:

Artisan::command('insert-tag', function () {

    $tagname = $this->ask('tagname: ');
    DB::table('tags')->insert(['tagname' => $tagname]);
    $this->comment("Tag inserted");
});

I jak widzimy created_at i updated_at nie aktualizują się bez Eloquenta. Ok, naprawmy to:

Artisan::command('update-tag', function () {

    
    DB::table('tags')->whereNull('created_at')->update(['created_at' => \Carbon\Carbon::now() ]);
    DB::table('tags')->whereNull('updated_at')->update(['updated_at' => \Carbon\Carbon::now() ]);
    
    $this->comment("Tags have timestamps now");
});

Ok, a z Eloquentem?

Artisan::command('create-tag', function () {

    $tagname = $this->ask('tagname: ');
    $tag = new Tag();
    $tag->tagname = $tagname;
    $tag->save();
    $this->comment("Tag inserted");
});

Z Eloquentem wszystko nam się pięknie wypełnia. Choć z fasadą DB też dałoby radę:

Artisan::command('insert-tag-with-time', function () {

    $tagname = $this->ask('tagname: ');

    DB::table('tags')->insert(
        [
            'tagname' => $tagname,
            'created_at' => \Carbon\Carbon::now(),
            'updated_at' => \Carbon\Carbon::now()
        ]
    );

    $this->comment("Tag inserted with timestamps");
});

Tej fasady DB nie ma co się bać, zwykłego SQLa podobnie (zwykłego PHP podobnie, operatorów bitowych podobnie, ASM pewnie też, ale tego nie próbowałem).

Ok, teraz kolejna migracja:

php artisan make:migration add_soft_delete_to_tags --table=tags

Dodajemy soft deletes. To jest migracja dodająca do już istniejącej tabeli, a zatem w down musimy się nieco bardziej postarać:

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

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::table('tags', function (Blueprint $table) {
            $table->dropSoftDeletes();
        });
    }
};

Ok, migrujemy. SoftDeletes dodaje nam pole deleted_at, które jest nullable i domyślnie null, czyli nic nie jest usunięte. Omawialiśmy to sobie w poprzednich lekcjach.

Ok, pora dodać softDeletes do modelu:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Tag extends Model
{
    use HasFactory;
    use SoftDeletes;
    protected $fillable = ['tagname'];
}

Piszemy komendę, usuwamy, patrzymy jak zmienia się w bazie danych:

Artisan::command('delete-tag {id}', function (int $id) {

    $tag = Tag::withoutTrashed()->findOrFail($id);
    $tag->delete();
    $this->comment("Tag deleted");

});

Teraz restore, te zasięgi też sobie omawialiśmy:

Artisan::command('restore-tag {id}', function (int $id) {

    $tag = Tag::onlyTrashed()->findOrFail($id);
    $tag->restore();
    $this->comment("Tag restored");

});

Ok, ale do pełni rozumienia potrzebujemy jeszcze zrobić to wszystko z fasadą DB (już czysty SQL możemy sobie darować, choć polecam w ramach ćwiczenia).

Fasada DB w odróżnieniu od Eloquenta jest akurat mnie fasadowa (w znaczeniu wzorzec projektowy) od Eloquenta.

Fasada DB akurat wymaga od nas dogłębnego rozumienia co framework Laravel nam wyczynia. Czyli musimy rozumieć co to ten withoutTrashed, albo co robi delete na modelach z soft deletes.

Oto rozwiązanie:

Artisan::command('delete-tag-db {id}', function (int $id) {

    $affectedRows = DB::table('tags')
    ->where('id', $id)
    ->whereNull('deleted_at')
    ->update(['deleted_at' => \Carbon\Carbon::now() ]);
    
    $this->comment("Tag deleted. Affected rows: {$affectedRows}");
});

Jeżeli na tym poziomie jeszcze nie ogarniamy tego, to musimy wrócić do starego materiału.

Teraz restore z DB:

Artisan::command('restore-tag-db {id}', function (int $id) {

    $affectedRows = DB::table('tags')
    ->where('id', $id)
    ->whereNotNull('deleted_at')
    ->update(['deleted_at' => null ]);
    
    $this->comment("Tag restored. Affected rows: {$affectedRows}");
});

To tylko rozgrzewka przed większą zabawą z relacjami, skoro tag to można się tego domyślić.