Mimo powszechnej dostępności modułów obsługujących komentarze, oferowanych przez serwisy społecznościowe, bywa że bardziej zależy nam na własnym, lokalnym rozwiązaniu, które da pełną kontrolę nad dodawanymi wpisami - od podstawowych kwestii związanych z prezentacją, aż po możliwość szybkiego wpływania na status każdego z nich.

Jeżeli zdecydujemy się na najprostsze rozwiązanie liniowo rejestrujące kolejne wpisy, czyli wszystkie kolejne wpisy należą do jednego wątku, bez kontroli hierarchii:

wpis->odpowiedź->odpowiedź->…,

to sprawa jest stosunkowo łatwa. Z pomocą prostego formularza dodajemy do tabeli kolejne wpisy, rejestrując przy tym wszystkie potencjalnie potrzebne informacje - treść, dane użytkownika, datę itp., a relację zbudujemy na polu głównego indeksu komentowanego obiektu.

Trochę bardziej skomplikowane jest rozwiązanie, w którym będziemy chcieli zachować hierarchię wątków i elegancko prezentować ją na stronie. W takiej sytuacji, na pierwszy rzut oka, koniecznym wydaje się zaangażowanie jakiegoś rekurencyjnego mechanizmu, który ze struktury PARENT<->children będzie budował np. tablicę z uporządkowanym hierarchicznie drzewem elementów. Jednak biorąc pod uwagę koszt, z jakim mogłoby to się wiązać zaproponuję inne rozwiązanie. Rekurencji co prawda nie unikniemy, ale wykorzystamy ją tylko na etapie wyświetlania.

Tabela

W pierwszej kolejności musimy przygotować tabelę i model ją reprezentujący. Zrobimy to oczywiście po "laravelowemu". W tym celu w katalogu głównym naszego projektu wykonujemy komendę:

php artisan make:model Models/Comment -m -c -r

gdzie

  • -m wymusza utworzenie pliku migracji,
  • -c wymusza utworzenie pliku kontrolera dla tworzonego modelu,
  • -r określa typ kontrolera jako "resource", pojawią się w nim wszystkie funkcje CRUD

Poprawnie wykonana komenda utworzy w odpowiednich katalogach naszego projektu wskazane pliki.

Zanim zaczniemy edytować właśnie utworzone pliki, musimy zaopatrzyć się w jeszcze jedno narzędzie. Zakładamy, że nasze komentarze mają być grupowane w wątkach i zgodnie z tym przyporządkowaniem elegancko wyświetlane na stronie. Możemy oczywiście pokusić się o samodzielne stworzenie odpowiednich funkcji w kontrolerze i nimi obsługiwać pobieranie komentarzy z bazy, ale wg mnie, zdecydowanie wygodniejszym rozwiązaniem będzie skorzystanie z jednego z dostępnych modułów wspierających programowanie tego typu algorytmów.

Jest to ważna decyzja, gdyż stosowanie wprost rekurencji może nieść poważne konsekwencje związane z wydajnością. Przy niewielkiej ilości pozycji nie będzie to zauważalne, ale gdy zasób stanie się duży, rekurencja może stać się przyczyną niepożądanych "zwisów". Rozwiązaniem może być na przykład zastosowanie techniki tzw. "Nested set model", dzięki której uwolnimy serwer z obowiązku mozolnego budowania hierarchii naszych komentarzy.

Dla Laravel5 jest dostępny pakiet, który zainstalujemy poleceniem:

composer require kalnoy/nestedset

które wykonujemy w katalogu głównym naszego projektu. Korzystamy z Laravel 5.5, więc nic więcej nie musimy robić, pakiet zarejestruje się automatycznie.

Teraz w katalogu database/migrations odszukajmy utworzony plik migracji o nazwie podobnej do:

database/migrations/2018_02_20_221837_create_comments_table.php

i wyedytujmy jego zawartość.

<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

class CreateCommentsTable extends Migration {

    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up() {
        Schema::create('comments', function(Blueprint $table) {
            $table->increments('id'); //indeks podstawowy
            $table->integer('user_id'); //kolumna relacji z tabelą users
            $table->integer('post_id'); //kolumna relacji z komentowanym postem
            $table->text('body', 65535); //treść komentarza
            $table->timestamps(); //generuje kolumny timestamps utworzenia i aktualizacji
            $table->softDeletes(); //generuje pole timestamp wykorzystywane przy "miękkim usuwaniu"

            // część magiczna - nested set model
            $table->nestedSet(); //utworzy wszystkie kolumny związane z obsługą NSM

        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down() {
        Schema::drop('comments');
    }

}

Sprawdzamy, czy wszystko jest ok i wykonujemy polecenie:

php artisan migrate

po którym w naszej bazie MySql zostanie utworzona tabela Comments o następującej strukturze:

Tabela Comments

Model

Gdy już mamy stworzoną tabelę możemy zająć się powiązanym z nią modelem. Model jest obiektową reprezentacją pojedynczego rekordu, a poprzez zdefiniowane metody może przetwarzać wartości kolumn przed ich dostarczeniem. W modelu definiujemy także relacje z innymi tabelami.

W naszym przypadku takie relacje będą dwie:

  1. z tabelą Users - pozwoli nam na identyfikację autora komentarza,
  2. z tabelą Posts - pozwili nam na powiązanie komentarza z artykułem.

Otwieramy w edytorze plik app/Http/Models/Comment.php i umieszczamy w nim niezbędne definicje:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Kalnoy\Nestedset\NodeTrait;
use Illuminate\Database\Eloquent\SoftDeletes;

class Comment extends Model {

    use NodeTrait;
    use SoftDeletes;

    // które kolumny możemy wypełniać
    protected $fillable = [
        'body',
        'user_id',
        'post_id',
    ];

    // relacja z tabelą Posts, wskazuje na artykuł, do którego należy komentarz
    public function post() {
        return $this->belongsTo(Post::class)->withDefault(function($comment) {
            $comment->id = 0;
        });
    }

    // relacja z tabelą Users, wskazuje na użytkownika, który jest autorem komentarza
    public function user() {
        return $this->belongsTo(User::class);
    }

}

W przypadku relcji z tabelą Posts dodałem metodę withDefault, ponieważ nie każdy artykuł będzie miał przypisany przynajmniej jeden komentarz, ustawiając w odpowiedzi wartość id na 0 zabezpieczam się przed generowanie niepotrzebnych błędów lub ostrzeżeń.

Oczywiście modele Post i Users także musimy uzupełnić definicjami odpowiednich relacji:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model {
    (...)
    public function comment() {
        return $this->hasMany(Comment::class);
    }
    (...)
}
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class User extends Model {
    (...)
    public function comment() {
        return $this->hasMany(Comment::class);
    }
    (...)
}

W obu przypadkach budujemy relacje ManyToMany - zarówno użytkownik, jak i dowolny artykuł mogą mieć powiązanie z wieloma komentarzami.

Może wygląda to odrobinę zbyt prosto, ale w gruncie rzeczy na tym etapie możemy przyjąć, że mamy zdefiniowaną główną część naszego systemu komentarzy. Funkcjonalnie to oczywiście bieda, ale logika jest już gotowa. Nie możemy jeszcze w pełni tego przetestować, ale w tym zakresie jakichś większych zmian już nie będzie.

Programowanie kontrolera opiszę w kolejnym artykule.


blog comments powered by Disqus