Темы (Threads)

Следующие маршруты из файла web.php относятся к темам:
Route::get('/', 'ThreadsController@index');
Route::get('threads/create', 'ThreadsController@create');
Route::get('threads/{channel}/{thread}', 'ThreadsController@show');
Route::delete('threads/{channel}/{thread}', 'ThreadsController@destroy');
Route::post('threads', 'ThreadsController@store');
Route::get('threads/{channel}', 'ThreadsController@index');
Как видим по коду, во всех маршрутах используются разные методы ThreadsController. Два маршрута используют один и тот же метод – index:
Route::get('/', 'ThreadsController@index');
Route::get('threads/{channel}', 'ThreadsController@index');
Первый маршрут отвечает за отображение главной страницей сайта со списком всех тем форума, второй маршрут используется для вывода списка тем, относящихся к определенной рубрике (channel). Код этого метода следующий:
public function index(Channel $channel, ThreadFilters $filters)
{
    $threads = $this->getThreads($channel, $filters);

    if (request()->wantsJson()) {
        return $threads;
    }
    return view('threads.index', compact('threads'));
}
Здесь переменная $threads использует метод getThreads($channel, $filters) для получения списка тем. Код этого метода следующий:
protected function getThreads(Channel $channel, ThreadFilters $filters)
{
    $threads = Thread::latest()->filter($filters);
    if ($channel->exists) {
        $threads->where('channel_id', $channel->id);
    }
    return $threads->get();
}
Вначале, в переменной $threads мы определяем параметры сортировки тем по дате (Thread::latest()); фильтрацию по пользователю, сортировку по популярности, фильтрацию и сортировку по содержанию ответов (filter($filters)). Методы фильтров содержатся в классе ThreadFilters, о котором будет сказано позже. Далее, атрибут $channel->exists указывает, существует ли модель данной рубрики. Если да, то выбираем темы, относящиеся к данной рубрике (с предыдущими параметрами фильтрации/сортировки), если нет, то выбираем все темы (с предыдущими параметрами фильтрации/сортировки).

Фильтрация по запросу (классы: Filters и ThreadFilters)
Выше, для получения нужного списка пользователей мы фильтровали темы по пользователю, сортировали по популярности, фильтровали и сортировали по содержанию ответов. Для этого мы использовали метод filter($filters).
$threads = Thread::latest()->filter($filters);
Это так называемая заготовка запроса или Local Scopes. Данный метод на самом деле находится в файле модели Thread.php и выглядит он следующим образом:
public function scopeFilter($query, ThreadFilters $filters)
{
    return $filters->apply($query);
}
Здесь используется метод apply($query) класса ThreadFilters. Содержимое которого следующее:
class ThreadFilters extends Filters
{
    protected $filters = ['by', 'popular', 'answered'];
    /*фильтрация по имени пользователя */
    protected function by($username)
    {
        $user = User::where('name', $username)->firstOrFail();

        return $this->builder->where('user_id', $user->id);
    }
    /*сортировка по популярности */
    protected function popular()
    {
        $this->builder->getQuery()->orders = [];

        return $this->builder->orderBy('replies_count', 'desc');
    }
    /*фильтрация (и сортировка) по количеству ответов */
    protected function answered()
    {
        $this->builder->getQuery()->orders = [];

        return $this->builder
            ->whereHas('replies')
            ->orderBy('replies_count', 'desc');
    }    
}
Здесь вы видите все три метода, нужные для фильтрации и/или сортировки. Метода apply($query) здесь нет, потому что он находится в родительском классе Filters (файл Filters.php). Его полный код следующий:
class Filters
{
    protected $request;
    protected $builder;
    protected $filters = [];

    public function __construct(Request $request)
    {
        $this->request = $request;
    }

    public function apply($builder)
    {
        $this->builder = $builder;

        foreach ($this->getFilters() as $filter => $value) {
            if (method_exists($this, $filter)) {
                $this->$filter($value);
            }
        }

        return $this->builder;
    }

    public function getFilters()
    {
        return $this->request->intersect($this->filters);
    }
}
Возвращаемся к контроллеру ThreadsController…
Его полный код будет следующим:
class ThreadsController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth')->except(['index', 'show']);
    }

    public function index(Channel $channel, ThreadFilters $filters)
    {
        $threads = $this->getThreads($channel, $filters);

        if (request()->wantsJson()) {
            return $threads;
        }

        return view('threads.index', compact('threads'));
    }

    public function create()
    {
        return view('threads.create');
    }

    public function store(Request $request)
    {
        $this->validate($request, [
            'title' => 'required',
            'body' => 'required',
            'channel_id' => 'required|exists:channels,id'
        ]);

        $thread = Thread::create([
            'user_id' => auth()->id(),
            'channel_id' => request('channel_id'),
            'title' => request('title'),
            'body' => request('body')
        ]);

        return redirect($thread->path());
    }

    public function show($channel, Thread $thread)
    {
        return view('threads.show', [
            'thread' => $thread,
            'replies' => $thread->replies()->paginate(20)
        ]);
    }

    public function destroy($channel, Thread $thread)
    {
        $this->authorize('update', $thread);

        $thread->delete();

        if (request()->wantsJson()) {
            return response([], 204);
        }

        return redirect('/threads');
    }

    protected function getThreads(Channel $channel, ThreadFilters $filters)
    {
        $threads = Thread::latest()->filter($filters);
        if ($channel->exists) {
            $threads->where('channel_id', $channel->id);
        }

        return $threads->get();
    }
}
Рассмотрим подробнее метод destroy($channel, Thread $thread). Приведу его код еще раз отдельно.
public function destroy($channel, Thread $thread)
{
    $this->authorize('update', $thread);

    $thread->delete();

    if (request()->wantsJson()) {
        return response([], 204);
    }

    return redirect('/threads');
}
Метод authorize('update', $thread) – это метод авторизации, использующий политику (Policy). Параметр 'update' будет содержаться в будущем классе ThreadPolicy как одноименный метод. Этот метод будет проверять, является ли данный пользователь создателем темы, указанной в параметре $thread. Если да, то тему можно удалить, если нет, то браузеру вернется ответ с кодом 403 Forbidden («запрещено»).

Для создания политики ThreadPolicy воспользуемся командой:
php artisan make:policy ThreadPolicy
И в созданном классе добавим метод update(). Полный код класса будет следующим:
class ThreadPolicy
{
    use HandlesAuthorization;

    public function update(User $user, Thread $thread)
    {
        return $thread->user_id === $user->id;
    }
}
После создания политики зарегистрируем ее в классе AuthServiceProvider (app/Providers/ AuthServiceProvider.php). Таким образом, массив $policies в этом классе будет выглядеть следующим образом:
protected $policies = [
    Thread::class => ThreadPolicy::class,
]; 
Модель Thread
Файл модели, как вы помните, уже был создан ранее. Содержимое класса модели должно быть следующее:
class Thread extends Model
{
    protected $guarded = [];
    
    /* Активная загрузка (eager loading)  */
    protected $with = ['creator', 'channel'];

    protected static function boot()
    {
        parent::boot();

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

        static::deleting(function ($thread) {
            $thread->replies()->delete();
        });
    }

    public function path()
    {
        return "/threads/{$this->channel->slug}/{$this->id}";
    }

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

    public function channel()
    {
        return $this->belongsTo(Channel::class);
    }

    public function replies()
    {
        return $this->hasMany(Reply::class);
    }

    public function addReply($reply)
    {
        $this->replies()->create($reply);
    }

    public function scopeFilter($query, ThreadFilters $filters)
    {
        return $filters->apply($query);
    }
} 
Мы видим здесь рассмотренный метод scopeFilter(). Также нам здесь интересен метод boot(). Приведу его код еще раз отдельно.
protected static function boot()
{
    parent::boot();

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

    static::deleting(function ($thread) {
        $thread->replies()->delete();
    });
}
В данном методе мы определяем глобальную заготовку или global scope. Благодаря этой заготовке мы получаем доступ к полю replies_count модели с количеством связанных сообщений. Данное поле мы использовали ранее, когда сортировали темы по популярности и фильтровали темы с сообщениями (см. ThreadFilters).

Метод deleting() – это событие. Когда удаляется тема, удаляются все связанные сообщения.