Laravel. Вложенные комментарии (Nested Comments)

Добавлено: 24/07/2019 12:11 |  Обновлено: 24/07/2019 12:11 |  Добавил: nick |  Просмотры: 32031 Комментарии: 4
Вводная часть
В этом материале вы узнаете, как добавить вложенные комментарии к своему сайту на Laravel. Код материала подходит для 5-ой версии фреймворка.
На скриншоте показан список постов с количеством комментариев к ним. На скриншоте ниже показан отдельный пост со списком комментариев к нему. Материал основан на коде разработчика Jeffrey Way. Оригинальный код можно найти на его GitHub-странице «laracasts/Nested-Comments» (https://github.com/laracasts/Nested-Comments).

В данном примере я использую БД SQLite, но вы можете использовать любую другую.

Для того, чтобы фреймворк «понял», что мы хотим работать с SQLite, нужно изменить настройки соединения с БД в файле .env. Откроем его, и в строке DB_CONNECTION пропишите значение sqlite. Остальные строки данной секции (DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORD) нужно удалить. Получится так:
DB_CONNECTION=sqlite
Не забудьте создать файл БД database.sqlite в папке database проекта.

Далее создадим 2 модели: Post и Comment, для постов и комментариев. Одновременно создадим 2 файла миграций для создания таблиц в БД. Для этого используем следующие 2 команды:
php artisan make:model Post -m
php artisan make:model Comment -m
После чего в папке migrations появятся 2 новых файла:
2019_05_17_121555_create_posts_table.php
2019_05_17_121620_create_comments_table.php
Естественно, время в названии файлов у вас будет другое.

Откроем первый файл и содержимое функции up() заменим на следующее:
Schema::create('posts', function (Blueprint $table) {
    $table->increments('id');
    $table->integer('user_id')->index();
    $table->string('title');
    $table->text('body');
    $table->timestamps();
});
Заменим также содержимое функции up() во втором файле:
Schema::create('comments', function (Blueprint $table) {
    $table->increments('id');
    $table->integer('user_id')->index();
    $table->integer('post_id')->index();
    $table->integer('parent_id')->index()->nullable();
    $table->text('body');
    $table->timestamps();
});
Далее, добавим встроенную регистрацию/аутентификацию с помощью artisan-команды:
php artisan make:auth
Это для того, чтобы потом можно было добавить комментарии от своего имени.

Добавим таблицы в БД с помощью artisan-команды:
php artisan migrate
Создадим файл ModelFactory.php в папке factories. В него нужно добавить следующее содержимое:
<?php

$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->safeEmail,
        'password' => bcrypt('secret'),
        'remember_token' => str_random(10),
    ];
});


$factory->define(App\Post::class, function (Faker\Generator $faker) {
    return [
        'user_id' => function () {
            return factory(App\User::class)->create()->id;
        },
        'title' => $faker->sentence,
        'body'  => $faker->paragraph
    ];
});

$factory->define(App\Comment::class, function (Faker\Generator $faker) {
    return [
        'user_id' => function () {
            return factory(App\User::class)->create()->id;
        },
        'post_id' => function () {
            return factory(App\Post::class)->create()->id;
        },
        'body' => $faker->paragraph
    ];
});
С помощью этого файла мы указываем, какими тестовыми данными мы хотим заполнить таблицы пользователей, постов и комментариев.

После чего в файл DatabaseSeeder.php, в метод run() нужно добавить следующую строчку:
factory(App\Comment::class, 10)->create();
С помощью этой строчки мы указываем, сколько нужно создать тестовых комментариев. При создании комментариев будут созданы также посты и пользователи.

Далее заполним таблицы тестовыми данными с помощью artisan-команды:
php artisan db:seed
Можно посмотреть созданные объекты комментариев через tinker. Для этого запустите artisan-команду:
php artisan tinker
Далее, нужно указать, что используется модель комментариев:
use App\Comment;
И получить список комментариев так:
Comment::get();
В итоге вы должны увидеть что-то подобное: Модели
Модель Post.php должна иметь следующее содержимое:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('commentCount', function ($builder) {
            $builder->withCount('comments');
        });
    }

    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    public function getThreadedComments()
    {
        return $this->comments()->with('owner')->get()->threaded();
    }

    public function addComment($attributes)
    {
        $comment = (new Comment)->forceFill($attributes);
        $comment->user_id = auth()->id();

        return $this->comments()->save($comment);
    }
}
Здесь в методе boot() мы добавляем глобальную заготовку запроса (Global Scope), для того чтобы вместе с постом выводить количество комментариев к нему.

Далее в методе getThreadedComments() мы получаем список комментариев, вместе с пользователями их добавившими (метод with('owner')). Комментарии здесь уже сгруппированы по отдельным веткам (метод threaded()).

Метод threaded() находится в файле CommentCollection.php. В этом файле класс CommentCollection расширяет класс Collection, потому что список комментариев на выходе получается в виде коллекции.

Содержимое файла CommentCollection.php должно быть следующим:
<?php
namespace App;
use Illuminate\Database\Eloquent\Collection;

class CommentCollection extends Collection
{
    public function threaded()
    {
        $comments = parent::groupBy('parent_id');

        if (count($comments)) {
            $comments['root'] = $comments[''];
            unset($comments['']);
        }
        return $comments;
    }
}
Содержимое файла Comment.php должно быть следующим:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    protected $fillable = ['body'];

    public function owner()
    {
        return $this->belongsTo(User::class, 'user_id');
    }

    public function newCollection(array $models = [])
    {
        return new CommentCollection($models);
    }
}
Маршруты
Файл web.php должен содержать следующие маршруты:
<?php
Route::get('/', function () {
    return view('posts.index')->with([
        'posts' => \App\Post::get()
    ]);
});

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

Route::get('posts/{post}', function (App\Post $post) {
    return view('posts.show')->with([
        'post' => $post,
        'comments' => $post->getThreadedComments()
    ]);
});


Route::post('posts/{post}/comments', function (App\Post $post) {
    $post->addComment([
        'body' => request('body'),
        'parent_id' => request('parent_id', null)
    ]);

    return back();
})->middleware('auth');
Виды
В папке views нужно создать 2 папки: posts и comments.

Папка posts должна содержать 2 файла: index.blade.php и show.blade.php. Первый файл для вывода списка постов, а второй для вывода содержимого одного поста вместе с комментариями.

Файл index.blade.php должен иметь следующее содержимое:
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                @forelse ($posts as $post)
                    <div class="panel panel-default">
                        <div class="panel-heading">
                            <div class="level">
                                <h4 class="flex">
                                    <a href="/posts/{{ $post->id }}">
                                        {{ $post->title }}
                                    </a>
                                </h4>

                                <a href="/posts/{{ $post->id }}">
                                    Комментариев: {{ $post->comments_count }}
                                </a>
                            </div>
                        </div>

                        <div class="panel-body">
                            {{ $post->body }}
                        </div>
                    </div>
                @empty
                    <p>Пока что здесь ничего нет.</p>
                @endforelse
            </div>
        </div>
    </div>
@endsection
Файл show.blade.php должен иметь следующее содержимое:
@extends('layouts.app')

@section('content')
    <div class="container">
        <div class="row">
            <div class="col-md-8 col-md-offset-2">
                <h1>{{ $post->title }}</h1>
                {{ $post->body }}
                <h3>Комментарии</h3>
                @include ('comments.list', ['collection' => $comments['root']])

                @if (Auth::check())
                    <h3>Оставьте свой комментарий</h3>
                    @include ('comments.form')
                @endif               
            </div>
        </div>
    </div>
@endsection
Папка comments должна содержать 3 файла: list.blade.php, comment.blade.php и form.blade.php. Первый файл нужен для вывода списка комментариев, второй для вывода отдельного комментария, а третий для вывода формы добавления нового комментария.

Файл list.blade.php должен иметь следующее содержимое:
@foreach ($collection as $comment)
    @include ('comments.comment')
@endforeach
Файл comment.blade.php должен иметь следующее содержимое:
<div class="panel panel-default" style="margin-top: 10px">
    <div class="panel-heading">
        <div class="level">
            <h4 class="flex">
                {{ $comment->owner->name }} пишет
            </h4>
        </div>
    </div>

    <div class="panel-body">
        {{ $comment->body }}
        @if (Auth::check())
            <a class="btn btn-link" data-toggle="collapse" href="#commentForm{{$comment->id}}" role="button" aria-expanded="false" aria-controls="collapseExample">
            Комментировать
            </a>
            <div class="collapse" id="commentForm{{$comment->id}}">
                @include ('comments.form', ['parentId' => $comment->id])
            </div>            
        @endif

        @if (isset($comments[$comment->id]))
            @include ('comments.list', ['collection' => $comments[$comment->id]])
        @endif         
    </div>
  
</div>
Файл form.blade.php должен иметь следующее содержимое:
<form method="POST" action="/posts/{{ $post->id }}/comments" style="margin: 10px 0 10px 0">
    {{ csrf_field() }}

    @if (isset($parentId))
        <input name="parent_id" type="hidden" value="{{ $parentId }}"></input>
    @endif
 	<div class="form-group">
    	<label for="commentBody">Текст комментария</label>
    	<textarea class="form-control" id="commentBody" name="body" required></textarea>
	</div>

    <button type="submit" class="btn btn-primary">Отправить</button>
</form>
На этом все. Можно попробовать зарегистрироваться и подобавлять комментарии от своего имени.

Оставьте свой комментарий

Комментарии

Есть комментарии, ожидающие модерации (2)...