Темы (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() – это событие. Когда удаляется тема, удаляются все связанные сообщения.