Laravel 9: Ваше первое приложение

Источник: «Your first Laravel 9 Application»
Если вы никогда не создавали Laravel приложение, позвольте показать вам пошаговое руководство по Laravel с нуля — без особых требований. Следуйте инструкциям, для изучения Laravel.

Laravel рос невероятными темпами с момента своего первого релиза и недавно добавили двух новых штатных сотрудников, которые помогают развивать экосистему фреймворка. Он никуда не денется в ближайшее время, так что мы могли бы попытаться изучить его, верно? Если вы ещё не сделали это. Laravel всегда был ориентирован на разработчиков, уделяя особое внимание опыту разработчиков, производительности и расширяемости. Если спросить любого разработчика Laravel, почему ему нравится Laravel, он почти всегда ответит, что это опыт разработчика. Итак, вопрос в том, зачем писать что-то ещё, когда на Laravel так приятно писать?!

Установка Laravel 9

Это руководство по Laravel предназначено для тех, кто только начал изучать Laravel. Может быть, они знают, что это такое, или пытались установить его пару раз и остановились, потому что почувствовали себя ошеломлёнными. Мы собираемся создать новое приложение Laravel с самого начала, и единственное, что вам нужно, это:

Так что же мы будем создавать? Мы собираемся сделать хранитель закладок, чтобы вы могли брать ссылки, которые вам интересны и сохранять их. Наряду с этим мы также позволим добавлять тэги к закладкам, для их классификации.

Как начать работу с Laravel? Первое, что нужно сделать, это, конечно же, создать новый проект и сделать это можно несколькими способами: через установщик Laravel, используя Laravel Sail Build или просто с помощью composer create-project. В этом руководстве по Laravel будем использовать composer create-project: хочу, чтобы требования оставались минимальными. Поэтому выберите каталог, в котором вы хотите разместить своё приложение, и выполните команду composer:

composer create-project laravel/laravel bookmarker

Теперь откройте новый каталог bookmarker в вашем редакторе кода, чтобы мы могли начать. Это пустой Laravel проект, наша отправная точка. Не буду делать никаких предположений о том, как вы хотите рассматривать этот проект локально, так как есть много разных вариантов. Вместо этого будем использовать artisan для обслуживания приложения. Запустите следующую команду artisan:

php artisan serve

Вы получите сообщение о том по какому адресу доступно ваше приложение. Откройте его в браузере. Это должен быть экран Laravel по умолчанию. Поздравляю, вы сделали первый шаг с Laravel! Далее мы перейдём к том, как работает это приложение.

Laravel загружает все маршруты из route/web.php, и у вас есть несколько вариантов маршрутизации. Вы можете загрузить представление напрямую, используя Route::view(), когда вам не нужно передавать данные в представление. Вы можете использовать вызов|Замыкание|функцию вызвав Route::get('route', fn () => view('home')) где get — HTTP-метод который вы хотите использовать. Вы также может использовать контроллеры, чтобы изолировать логику внутри одного класса Route::get('route', App\Http\Controllers\SomeRouteController::class).

По поводу загрузки маршрутов через контроллеры тоже есть варианты. Вы можете объявить их как строки и указать на определённые методы Route::get('route', 'App\Http\Controllers\SomeController@methodName'). Вы можете объявить ресурсы маршрута, где Laravel примет стандартный Route::resource('route', 'App\Http\Controllers\SomeController'), который будет сдержать методы index, create, store, show, edit, update и destroy. Они очень подробно объяснены в документации. Вы также можете использовать вызываемые контроллеры, представляющие собой класс с одним методом __invoke(), который обрабатывается как Замыкание Route::get('route', App\Http\Controllers\SomeController::class).

Подключение базы данных

В этом руководстве по Laravel будем использовать вызываемые контроллеры, так как мне нравиться их использовать. Мне нравиться использовать их, потому что они сохраняют мои маршруты в чистоте, удобны для моей IDE и инкапсулируют каждый маршрут в отдельный класс.

Начиная любой новый проект, вы должны понимать, что вы от него хотите. Как было сказано, мы создаём приложения для хранения закладок, поэтому мы можем предположить, что мы хотим, чтобы наше приложение делало. Напишем несколько требований:

Эти требования — почти пользовательские истории, поэтому мы можем их проработать и понять области, которые, возможно, захотим затронуть. Наш первый шаг — проектирование данных, которые нам нужно хранить в базе данных. Мы будем использовать SQLite для хранения данных этого приложения, чтобы наши требования оставались низкими.

Чтобы начать работать с SQLite в нашем Laravel приложении, во-первых, нам нужно создать SQLite файл в database/database.sqlite это можно сделать в терминале или IDE.

Затем нам нужно открыть .env файл и изменить блок database, что бы наше приложение знало о нашей базе данных. Laravel использует env файл для настройки локальной среды, которая будет загружена через различные файлы конфигурации в config/*.php. Каждый файл настраивает определённые части вашего приложения, поэтому не стесняйтесь потратить немного времени на изучение этих файлов и посмотрите как работает конфигурация.

На данный момент в вашем .env файле будет блок который выглядит следующим образом:

DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=

Теперь нам нужно внести в этот блок следующие изменения. Изменить DB_DATABASE на полный путь к SQLite файлу database/database.sqlite. В DB_CONNECTION указать sqlite и добавить строку DB_FOREIGN_KEYS=true. Остальное можно удалить, у вас должно получиться что-то вроде:

DB_CONNECTION=sqlite
DB_DATABASE=/Users/steve/code/sites/bookmarker/database/database.sqlite
DB_FOREIGN_KEYS=true

Мы установили соединение с базой данных SQLite. Установили путь к файлу базу и указали, что хотим, чтобы SQLite включил внешние ключи.

Когда у нас настроена и сконфигурирована база данных, мы можем запустить миграцию базы данных по умолчанию. В Laravel миграция базы данных используется для обновления состояния базы данных вашего приложения. Каждый раз, когда вы хотите изменить структуру базы данных, вы создаёте новую миграцию для создания таблицы, добавления или удаления полей, или даже полного удаления таблицы. Документация по миграции базы данных превосходна и объясняет все доступные варианты, поэтому когда будет время — обязательно её прочтите. Так же вам стоит прочитать статью Laravel: Все секреты миграции. По умолчанию Laravel поставляется с несколькими миграциями для пользователей, сброса пароля, неудачных заданий и PAT. Они полезны в 99% приложений, поэтому мы оставим их как есть.

К счастью, в Laravel есть готовая модель User, поэтому нам не нужно ничего редактировать или менять. Мы собираем имена пользователей, адреса электронной почты и пароли, сохраняем время создания и обновления пользователя. Итак, у нас есть уже готовая модель данных. Нам нужно подумать о том, как этот пользователь может получить доступ к нашему приложению. Нужно, что бы он мог войти в систему или зарегистрировать новую учётную запись. Для этого в Laravel есть несколько доступных пакетов, или вы можете создать свою собственную аутентификацию. Стандартные пакеты превосходны и настраиваются, поэтому будем использовать их.

Для этого приложения будем использовать Breeze, который представляет базовую основу аутентификации. Но есть и другие варианты, такие как Jetstream позволяющие использовать 2FA и модель команд, в которой могут сотрудничать несколько человек. Существует ещё один пакет под названием Sociality, который позволяет настраивать вход через социальные сети от множества поставщиков. Однако нам это не нужно, поэтому становите Laravel Breeze с помощью следующей команды:

composer require laravel/breeze --dev

Это делает Laravel Breeze зависимостью разработки для приложения, и это зависимость разработки, потому что её нужно установить. После установки, пакет копирует файлы в ваше приложение для маршрутизации, представлений, контроллеров и прочего. Итак, давайте установим пакет, используя следующую команду artisan:

php artisan breeze:install

Наконец, нам нужно установить и собрать фронтенд ресурсы с помощью npm:

npm install && npm run dev

Этот процесс займёт некоторое время, так как необходимо загрузить все пакеты JavaScript или CSS, затем запустить процесс сборки. Как только это будет сделано скрипт завершит свою работу.

Всё установлено и готово. Нужно запустить миграцию базы данных, что бы наша база находилась в определённом состоянии, с которым мы могли бы начать работать. Вы можете сделать это выполнив команду artisan:

php artisan migrate

Эта команда выполнит каждую миграцию, которая содержится в каталоге database/migrations, и применит их к базе данных. Таким образом, состояние базы данных можно напрямую связать с системой управления версиями, что сделает ваше приложение более умным и устойчивым.

Давайте на минуту задумаемся о том, как мы хотим хранить закладки. Каждая закладка должна принадлежать пользователю, иметь уникальный идентификатор, URL-адрес, который можно посетить, и необязательно описание на случай, если вы хотите написать заметку о том, для чего она нужна.

Создание моделей Eloquent

Теперь мы можем сгенерировать новую Eloquent модель и выполнить миграцию с помощью командной строки artisan. Выполните в терминале следующее:

php artisan make:model Bookmark -m

Мы говорим Laravel создать новую модель Eloquent с названием Bookmark, а флаг -m указывает также сгенерировать миграцию. Если вам когда-нибудь понадобится создать новую модель и выполнить миграцию, рекомендуется использовать этот подход, поскольку он выполняет и то, и другое одновременно. Вы так же можете добавить к этой команде другие флаги для создания фабрик моделей, наполнителей и т.д., но мы не будем использовать их в этом руководстве по Laravel.

Это создаст новую миграцию внутри database/migrations, у неё будет временная метка, за которой следует create_bookmarks_table. Откройте её в своей IDE, что бы мы могли структурировать данные. В методе up замените содержимое следующим блоком:

Schema::create('bookmarks', static function (Blueprint $table): void {
$table->id();

$table->string('name');
$table->string('url');
$table->text('description')->nullable();

$table->foreignId('user_id')
->index()->constrained()->cascadeOnDelete();

$table->timestamps();
});

Из приведённого выше фрагмента кода вы можете увидеть описательный характер миграции базы данных, то, как мы создадим новую таблицу, и описание того, как мы хотим, чтобы она была создана. Теперь мы можем применить эти изменения к нашей базе данных, вновь запустив команду миграции artisan:

php artisan migrate

Затем, давайте перейдём к нашей Eloquent Модели и добавим код, что бы она знала о полях базы данных и любых отношениях, которые они могут иметь. Откройте app/Models/Bookmark.php и замените содержимое следующим кодом:

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

class Bookmark extends Model
{
use HasFactory;

protected $fillable = [
'name',
'url',
'description',
'user_id'
];

public function user(): BelongsTo
{
return $this->belongsTo(
related: User::class,
foreignKey: 'user_id',
);
}
}

Мы установили fillable атрибуты в соответствии с полями, доступными в таблице. Это остановит любые проблемы с массовым назначением атрибутов. Затем мы добавили метод user который является отношением. Запись Bookmark BelongsTo User (буквально: Закладка принадлежит Пользователю), использующему внешний ключ user_id. Мы можем добавить отношение к нашей модели User, поэтому в каталоге Models откройте файл User.php и замените содержимое следующим кодом:

declare(strict_types=1);

namespace App\Models;

use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
use Notifiable;
use HasFactory;
use HasApiTokens;

protected $fillable = [
'name',
'email',
'password',
];

protected $hidden = [
'password',
'remember_token',
];

protected $casts = [
'email_verified_at' => 'datetime',
];

public function bookmarks(): HasMany
{
return $this->hasMany(
related: Bookmark::class,
foreignKey: 'user_id',
);
}
}

Теперь пользователь знает об отношениях к закладкам как User HasMany Bookmarks (буквально: у Пользователя Много Закладок). Мы будем использовать эти отношения чуть позже, когда начнём строить логику в нашем приложении.

Наконец, мы можем создать модель Тэг. Мы хотим, что бы с каждой закладкой было связано много тэгов tags. Возьмём в качестве примера Laravel News. Мы могли бы поставить следующие тэги:

Таким образом, каждый раз когда мы хотим посмотреть закладки отмеченные любым из этих тэгов, должна появиться закладка Laravel News. Как и раньше, мы собираемся запустить команду artisan для создания модели Tag:

php artisan make:model Tag -m

Теперь откройте файл миграции в текстовом редакторе и снова замените содержимое метода up:

Schema::create('tags', static function (Blueprint $table): void {
$table->id();
$table->string('name');
$table->string('slug')->unique();
});

У наших тэгов есть name и slug, на этот раз нам не нужны временные метки, так как это не важная информация. Я называю это мета моделью, используемой для категоризации и, в основном, системой, пользователь создаёт их, но они не в центре внимания.

Итак, давайте поработаем над Eloquent моделью Tag:

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Tag extends Model
{
use HasFactory;

protected $fillable = [
'name',
'slug',
];

public $timestamps = false;
}

Пока никаких отношений не настраиваем, нам понадобится сводная таблица, для связывания тэгов с закладками. Запустите команду генерации миграции в терминале:

php artisan make:migration create_bookmark_tag_table

В Laravel есть соглашение, согласно которому для названия для сводных таблиц вы задаёте имя указав связываемые таблицы в алфавитном порядке и единственном числе. Итак, мы хотим объединить таблицы bookmarks и tags, поэтому мы называем сводную таблицу bookmark_tag поскольку теги могут принадлежать множеству разных закладок, а закладки могут иметь много тегов.

Давайте заполним эту миграцию, чтобы увидеть, чем она отличается, снова сосредоточившись на методе up:

Schema::create('bookmark_tag', static function (Blueprint $table): void {
$table->foreignId('bookmark_id')->index()->constrained()->cascadeOnDelete();
$table->foreignId('tag_id')->index()->constrained()->cascadeOnDelete();
});

Эта таблица должна содержать внешние ключи для закладок и первичные ключи тэгов. Теперь у нас есть Eloquent модель для этой таблицы, поэтому мы добавляем отношения в модель 'Tag' и Bookmark.

Теперь ваша Модель Tag должна выглядеть следующим образом:

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Tag extends Model
{
use HasFactory;

protected $fillable = [
'name',
'slug',
];

public $timestamps = false;

public function bookmarks(): BelongsToMany
{
return $this->belongsToMany(
related: Bookmark::class,
table: 'bookmark_tag',
);
}
}

Ваша Модель Bookmark должна выглядеть так:

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Bookmark extends Model
{
use HasFactory;

protected $fillable = [
'name',
'url',
'description',
'user_id'
];

public function user(): BelongsTo
{
return $this->belongsTo(
related : User::class,
foreignKey: 'user_id',
);
}

public function tags(): BelongsToMany
{
return $this->belongsToMany(
related: Tag::class,
table: 'bookmark_tag',
);
}
}

Наконец выполните миграцию для обновления состояния базы данных:

php artisan migrate

Создание пользовательского интерфейса

Теперь, когда наши модели Bookmark и Tag знакомы друг с другом, мы можем приступить к созданию пользовательского интерфейса! Мы не будем сосредотачиваться на отточенном пользовательском интерфейсе, поэтому не стесняйтесь проявлять творческий подход. Однако мы будем использовать tailwindcss.

Мы собираемся сделать большую часть нашей работы для закладок в маршруте панели управления (dashboard), созданном Laravel Breeze, поэтому, если вы посмотрите в routes/web.php вы должны увидеть следующее:

declare(strict_types=1);

use Illuminate\Support\Facades\Route;

Route::get('/', function () {
return view('welcome');
});

Route::get('/dashboard', function () {
return view('dashboard');
})->middleware(['auth'])->name('dashboard');

require __DIR__.'/auth.php';

Маршрут панели управления в настоящее время замыкание, и мы, вероятно, хотим преобразовать его в контроллер. Итак, давайте создадим новый контроллер, чтобы сохранить логику для панели управления, запустив следующую команду artisan:

php artisan make:controller DashboardController --invokable

Теперь давайте реорганизуем файл маршрутов, чтобы он стал немного чище:

declare(strict_types=1);

use Illuminate\Support\Facades\Route;

Route::view('/', 'welcome')->name('home');

Route::get(
'/dashboard',
App\Http\Controllers\DashboardController::class
)->middleware(['auth'])->name('dashboard');

require __DIR__.'/auth.php';

Мы упростили маршрут home до view маршрута, а маршрут dashboard теперь указывает на контроллер. Откройте этот контроллер в редакторе, чтобы можно было вставить логику из приведённого ниже кода:

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Models\Bookmark;
use Illuminate\Contracts\View\View;
use Illuminate\Http\Request;

class DashboardController extends Controller
{
public function __invoke(Request $request): View
{
return view('dashboard', [
'bookmarks' => Bookmark::query()
->where('user_id', auth()->id())
->get()
]);
}
}

Как и раньше, всё что нам нужно сделать это прямо сейчас вернуть представление. Теперь давайте проверим это, запустив следующую команду artisan, для запуска вашего приложения:

php artisan serve

Теперь, если вы откроете своё приложение в браузере, вверху справа вы должны увидеть две ссылки Login и Register (Вход и Регистрация). Попробуйте зарегистрировать учётную запись и подождите пока она перенаправит в панель управления. Вы должны увидеть сообщение You're logged in!Вы вошли!.

Фантастическая работа продолжается! У вас есть приложение Laravel обрабатывающее аутентификацию и модели данных в фоновом режиме, которые можно использовать для создания закладок и управления ими.

Laravel blade

Когда дело доходит до фронтэнда, могут появиться затруднения в выборе, так как существуют миллионы библиотек на JavaScript, есть обычный PHP и Blade, которые вы можете использовать. В этой части руководства мы сосредоточимся на использовании Laravel Blade, поскольку не хотим лишних сложностей или дополнительных пакетов на раннем этапе обучения.

Когда мы установили Laravel Breeze, мы получили несколько дополнительных файлов представления, и это здорово, поскольку он уже настроен. Для новой формы закладок я создам новый компонент Blade, который представляет собой отдельное представление view, которое мы можем использовать в нескольких местах.

Создайте анонимный компонент Blade, который является просто файлом представления, выполнив следующую команду:

php artisan make:component bookmarks.form --view

Затем внутри нашего resources/views/dashboard.blade.php проведём рефакторинг, что бы он выглядел следующим образом:

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }}
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<x-bookmarks.form :bookmarks="$bookmarks" />
</div>
</div>
</div>
</x-app-layout>

Мы загружаем blade компонент, вызывая <x-bookmarks.form />, и вот как это работает: Все blade компоненты можно загружать добавляя к имени префикс x-. Если он расположен в подкаталоге, мы обозначаем каждый подкаталог точкой ., поэтому глядя на x-bookmarks.form, мы можем предположить, что он хранится в resources/views/components/bookmarks/form.blade.php. В нём мы собираемся сделать простой способ добавления новых закладок. Добавьте следующий (массивный) фрагмент кода внутрь компонента:

@props(['bookmarks'])
<div>
<div x-data="{ open: true }" class="overflow-hidden">
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
<div class="-ml-4 -mt-2 flex items-center justify-between flex-wrap sm:flex-nowrap">
<div class="ml-4 mt-2">
<h3 class="text-lg leading-6 font-medium text-gray-900">
Your Bookmarks
</h3>
</div>
<div class="ml-4 mt-2 flex-shrink-0">
<a x-on:click.prevent="open = ! open" class="relative inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<span x-show="! open" x-cloak>Show Form</span>
<span x-show="open" x-cloak>Hide Form</span>
</a>
</div>
</div>
</div>
<div x-show="open" x-cloak class="divide-y divide-gray-200 py-4 px-4">
<div class="pt-8">
<div>
<h3 class="text-lg leading-6 font-medium text-gray-900">
Create a new bookmark.
</h3>
<p class="mt-1 text-sm text-gray-500">
Add information about the bookmark to make it easier to understand later.
</p>
</div>
<form id="bookmark_form" method="POST" action="{{ route('bookmarks.store') }}" class="mt-6 grid grid-cols-1 gap-y-6 gap-x-4 sm:grid-cols-6">
@csrf
<div class="sm:col-span-3">
<label for="name" class="block text-sm font-medium text-gray-700">
Name
</label>
<div class="mt-1">
<input type="text" name="name" id="name" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">
</div>
@error('name')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
<div class="sm:col-span-3">
<label for="url" class="block text-sm font-medium text-gray-700">
URL
</label>
<div class="mt-1">
<input type="text" name="url" id="url" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md">
</div>
@error('url')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
<div class="sm:col-span-6">
<label for="description" class="block text-sm font-medium text-gray-700">
Description
</label>
<div class="mt-1">
<textarea id="description" name="description" rows="3" class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border border-gray-300 rounded-md"></textarea>
</div>
<p class="mt-2 text-sm text-gray-500">
Write any notes about this bookmark.
</p>
@error('description')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
<div class="sm:col-span-6">
<label for="tags" class="block text-sm font-medium text-gray-700">
Tags
</label>
<div class="mt-1">
<input
type="text"
name="tags"
id="tags"
class="shadow-sm focus:ring-indigo-500 focus:border-indigo-500 block w-full sm:text-sm border-gray-300 rounded-md"
/>
<p class="mt-2 text-sm text-gray-500">
Add a comma separated list of tags.
</p>
@error('tag')
<p class="mt-2 text-sm text-red-500">
{{ $message }}
</p>
@enderror
</div>
</div>
<div class="sm:col-span-6">
<div class="pt-5">
<div class="flex justify-end">
<a x-on:click.prevent="document.getElementById('bookmark_form').reset();" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</a>
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Save
</button>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
@forelse ($bookmarks as $bookmark)
<div>
<a href="#" class="block hover:bg-gray-50">
<div class="px-4 py-4 sm:px-6">
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-600 truncate">
{{ $bookmark->name }}
</p>
</div>
<div class="mt-2 sm:flex sm:justify-between">
<div class="flex space-x-4">
@foreach ($bookmark->tags as $tag)
<p class="flex items-center text-sm text-gray-500">
{{ $tag->name }}
</p>
@endforeach
</div>
</div>
</div>
</a>
</div>
@empty
<a href="#" class="relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path></svg>
<span class="mt-2 block text-sm font-medium text-gray-900">
Create a new bookmark
</span>
</a>
@endforelse
</div>

У нас получился довольно большой компонент, который будет обрабатывать всю логику необходимую для создания закладки во фронтэнде. Начнём с того, что наш компонент объявляет какие атрибуты будут рассматриваться как переменные данных @props(['bookmarks']), которые может использовать компонент. Это просто переменные которые мы передаём, чтобы наш компонент знал о них. Затем у нас есть секция вверху, которая является секцией управления у неё есть заголовок и кнопка действия. Мы используем [Alpine.js](https://alpinejs.dev/) для базового JavaScript, который нам нужен — переключение видимости формы. Наша форма — стандартная HTML-форма, но мы отправляем её данные по маршруту, который нам ещё предстоит создать, мы скоро его добавим. Затем мы добавляем в форму новую blade директиву @csrf, эта директива предотвращает CSRF с которыми мы можем столкнуться, если другие сайты попытаются вмешаться и взломать нашу форму. Остальной код — просто разметка визуальных элементов, поэтому не стесняйтесь настраивать его по своему усмотрению. Следует отметить, что в настоящее время мы добавляем тэги используя список тэгов разделённый запятыми. Мы могли бы подойти к этому немного по другому, если бы использовали больше JavaScript или UI-библиотек. Далее у нас идут кнопки отмены и сохранения. Кнопка отмены — сбрасывает форму с помощью JavaScript, а кнопка отправки, как вы можете догадаться, отправляет форму.

Создание контроллера

Итак, теперь мы должны сохранить данные формы, скорее всего, ваша страница не загрузится, потому что маршрут ещё не определён — и это нормально. Мы собираемся его создать. Однако сначала нам нужно создать контроллер, в который мы собираемся сохранить эти данные. С помощью следующей команды artisan создайте новый контроллер:

php artisan make:controller Bookmarks/StoreController --invokable

Затем добавим в файл маршрутов следующее:

Route::post(
'bookmarks',
App\Http\Controllers\Bookmarks\StoreController::class,
)->middleware(['auth'])->name('bookmarks.store');

Теперь внутри контроллера нужно сделать несколько вещей, Во-первых, мы хотим проверить запрос, чтобы мы могли передать информацию обратно с сообщением валидации. Затем мы хотим выполнить действие для создания новой закладки, и наконец, перенаправить пользователя обратно в панель управления, где он увидит только что созданную закладку.

Внутри app/Http/Controllers/Bookmarks/StoreController.php добавим следующий код:

declare(strict_types=1);
namespace App\Http\Controllers\Bookmarks;
use App\Http\Controllers\Controller;
use App\Models\Tag;
use Illuminate\Http\Request;
use Illuminate\Http\RedirectResponse;
class StoreController extends Controller
{
public function __invoke(Request $request): RedirectResponse
{
$this->validate($request, [
'name' => [
'required',
'string',
'min:1',
'max:255',
],
'url' => [
'required',
'url',
],
'description' => [
'nullable',
'string',
],
'tags' => [
'nullable',
'array',
]
]);
$bookmark = auth()->user()->bookmarks()->create([
'name' => $request->get('name'),
'url' => $request->get('url'),
'description' => $request->get('description'),
]);
foreach (explode(',', $request->get('tags')) as $tag) {
$tag = Tag::query()->firstOrCreate(
['name' => trim(strtolower($tag))],
);
$bookmark->tags()->attach($tag->id);
}
return redirect()->route('dashboard');
}
}

У нас есть метод __invoke, который примет текущий запрос. Он обрабатывается Laravel DI контейнером, так что вам не о чем беспокоиться. Основная причина, по которой мы вызываем $this->validate заключается в том, что мы расширяем основной Контроллер для нашего Laravel приложения. Установим правила валидации. Первый аргумент, передаваемый для валидации — данные, которые мы хотим проверить. Затем мы передаём массив правил валидации, которым нужно следовать. Я установил правила в соответствии с разумными значениями по умолчанию и не стеснялся использовать доступные опции валидации из документации Laravel.

Затем мы переходим к созданию нашей закладки. Мы не используем модель, так как можем сэкономить время, получая аутентифицированного пользователя, получая отношения закладок и вызывая create — — значит нам не нужно передавать user_id, так как он доступен непосредственно из auth()->user(). Затем мы циклически перебираем теги из запроса и либо получаем первый соответствующий, либо создаём новый по введённому имени (из которого мы удаляем лишние пробелы и преобразуем в нижний регистр для согласованности). Затем, мы прикрепляем этот новый тег к закладке. Наконец, мы возвращаем redirect()->route('dashboard'), чтобы перенаправить пользователя в панель управления с только что созданной закладкой.

Рефакторинг кода контроллера

Код хорош и делает именно то, что нам нужно, но можем ли мы его улучшить? Я думаю да.

Выносим валидацию в запрос формы

Первый шаг в рефакторинге кода — вынести валидацию из контроллера. Нет ничего плохого в том, чтобы хранить её в контроллере. Однако мы можем сэкономить немного времени, не создавая экземпляр контроллера в случае сбоя проверки. Для этого мы можем создать новый запрос формы, используя следующую команду artisan:

php artisan make:request Bookmarks/StoreRequest

Это создаст новый класс в app/Http/Requests/Bookmarks/StoreRequest.php. Давайте откроем его, добавим код и пройдёмся по нему:

declare(strict_types=1);
namespace App\Http\Requests\Bookmarks;
use Illuminate\Foundation\Http\FormRequest;
class StoreRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
public function rules(): array
{
return [
'name' => [
'required',
'string',
'min:1',
'max:255',
],
'url' => [
'required',
'url',
],
'description' => [
'nullable',
'string',
],
'tags' => [
'nullable',
'array',
]
];
}
}

Мы используем метод authorize() для проверки является ли авторизированным запрос. На данный момент это нормально, но если вы добавите уровень ролей и разрешений, позже вы сможете гарантировать, что аутентифицированному пользователю разрешено выполнять действие store в закладках. Затем у нас есть метод rules, массив правил проверки, подобный тому, как был у нас в контроллере. Что Laravel будет делать сейчас, используя контейнер DI, когда приходит запрос — прежде чем он создаст экземпляр нового контроллера, он попытается создать запрос формы. Это проверит запрос. Если валидация не пройдёт, будет выдано исключение, которое Laravel поймает, преобразует для вас в ErrorBagи вернутся к предыдущему представлению с этим пакетом ошибок, доступным для отображения любых ошибок валидации. Очень полезная функция Laravel. Но, прежде чем это произойдёт, нам нужно указать нашему контроллеру использовать новый запрос формы, поэтому изменим сигнатуру метода __invoke, следующим образом:

public function __invoke(StoreRequest $request): RedirectResponse

Неудачная валидация нас не затронет. Поэтому мы можем удалить расширение базового контроллера Laravel и удалить ручную проверку:

declare(strict_types=1);
namespace App\Http\Controllers\Bookmarks;
use App\Http\Requests\Bookmarks\StoreRequest;
use App\Models\Tag;
use Illuminate\Http\RedirectResponse;
class StoreController
{
public function __invoke(StoreRequest $request): RedirectResponse
{
$bookmark = auth()->user()->bookmarks()->create([
'name' => $request->get('name'),
'url' => $request->get('url'),
'description' => $request->get('description'),
]);
foreach (explode(',', $request->get('tags')) as $tag) {
$tag = Tag::query()->firstOrCreate(
['name' => trim(strtolower($tag))],
);
$bookmark->tags()->attach($tag->id);
}
return redirect()->route('dashboard');
}
}

Внезапно наш контроллер стал намного меньше и его легче понять. Если вам нужно добавить примечания в комментариях, чтобы напомнить о выполнении валидации, не стесняйтесь, пока это не войдёт в рабочий процесс, в котором вы будете помнить что здесь происходит.

Выносим логику в Action

Мы могли бы оставить всё как есть, так как это разумное решение, но стандарт Laravel состоит в том, чтобы переместить логику создания в новый класс, называемый Action.

Не существует специальной команды для создания Action, нужно сделать это вручную. Создайте в каталоге app/Actions/Bookmarks/ новый файл CreateBookmarkAndTags.php.

Затем откройте этот файл в редакторе и добавьте следующий код:

declare(strict_types=1);
namespace App\Actions\Bookmarks;
use App\Models\Bookmark;
use App\Models\Tag;
class CreateBookmarkAndTags
{
public function handle(array $request, int $id): void
{
$bookmark = Bookmark::query()->create([
'name' => $request['name'],
'url' => $request['url'],
'description' => $request['description'],
'user_id' => $id,
]);
if ($request['tags'] !== null) {
foreach (explode(',', $request['tags']) as $tag) {
$tag = Tag::query()->firstOrCreate(
['name' => trim(strtolower($tag))],
);
$bookmark->tags()->attach($tag->id);
}
}
}
}

У нас есть один метод handle() принимающий данные запроса и идентификатор, который мы будем использовать в качестве идентификатора пользователя, затем копируем логику из контроллера и вносим несколько незначительных изменений. Мы можем использовать этот action класс в любом месте нашего приложения, из пользовательского интерфейса, из командной строки, или даже API, если это необходимо. Мы создали модульно действие, которое можно легко вызвать, протестировать и получить предсказуемые результаты.

Итак, теперь мы можем ещё больше облегчить контроллер:

declare(strict_types=1);
namespace App\Http\Controllers\Bookmarks;
use App\Actions\Bookmarks\CreateBookmarkAndTags;
use App\Http\Requests\Bookmarks\StoreRequest;
use Illuminate\Http\RedirectResponse;
class StoreController
{
public function __invoke(StoreRequest $request): RedirectResponse
{
(new CreateBookmarkAndTags())->handle(
request: $request->all(),
id: auth()->id(),
);
return redirect()->route('dashboard');
}
}

Теперь у нас есть один action класс, который мы вызываем внутри контроллера, а затем возвращаем перенаправление. Намного чище и с хорошим названием. Конечно, мы можем пойти дальше, если захотим. Используя контейнер Laravel для внедрения action в конструктор — это позволит нам вызывать action класс. Это будет выглядеть следующим образом.

declare(strict_types=1);
namespace App\Http\Controllers\Bookmarks;
use App\Actions\Bookmarks\CreateBookmarkAndTags;
use App\Http\Requests\Bookmarks\StoreRequest;
use Illuminate\Http\RedirectResponse;
class StoreController
{
public function __construct(
protected CreateBookmarkAndTags $action,
) {}
public function __invoke(StoreRequest $request): RedirectResponse
{
$this->action->handle(
request: $request->all(),
id: auth()->id(),
);
return redirect()->route('dashboard');
}
}

Этот последний метод полезен, если у вашего action класса есть требования к его конструктору. Скажем вы используете шаблон Репозитория или другой шаблон — вы можете добавить его в конструктор своего action и Laravel решит это автоматически, если сможет.

Создание контроллера удаления закладок

Таким образом, мы можем показать список и добавить закладки, и мы можем добавить в список кнопку удаления — не смысла создать что-то слишком больше, верно?

Создадим новый контроллер следующей командой:

php artisan make:controller Bookmarks/DeleteController --invokable

Теперь нам не нужен action, так как это однострочное действие, но если вы хотите предоставить несколько способов удаления закладок, выполните те же действия, что и выше, но на этот раз удалите закладку, а не создайте её. Добавьте следующий код в контроллер:

declare(strict_types=1);
namespace App\Http\Controllers\Bookmarks;
use App\Models\Bookmark;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class DeleteController
{
public function __invoke(Request $request, Bookmark $bookmark): RedirectResponse
{
$bookmark->delete();
return redirect()->route('dashboard');
}
}

Здесь мы принимаем модель Bookmark в качестве аргумента, чтобы мы могли включить привязку модели маршрута, где Laravel будет искать запись для вас и внедрять её в ваш метод — в случае сбоя он выдаст исключение 404. Всё что нам нужно сделать, это вызвать удаление модели и вернуть перенаправление. Добавьте следующий маршрут:

Route::delete(
'bookmarks/{bookmark}',
App\Http\Controllers\Bookmarks\DeleteController::class,
)->middleware(['auth'])->name('bookmarks.delete');

Наконец, мы можем вернуться к нашему компоненту и добавить кнопку:

@forelse ($bookmarks as $bookmark)
<div>
<a href="#" class="block hover:bg-gray-50">
<div class="px-4 py-4 sm:px-6">
<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-600 truncate">
{{ $bookmark->name }}
</p>
<div class="ml-2 flex-shrink-0 flex">
<form method="DELETE" action="{{ route('bookmarks.delete', $bookmark->id) }}">
@csrf
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-red-500 bg-gray-100 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Delete
</button>
</form>
</div>
</div>
<div class="mt-2 sm:flex sm:justify-between">
<div class="flex space-x-4">
@foreach ($bookmark->tags as $tag)
<p class="flex items-center text-sm text-gray-500">
{{ $tag->name }}
</p>
@endforeach
</div>
</div>
</div>
</a>
</div>
@empty
<a href="#" class="relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z"></path></svg>
<span class="mt-2 block text-sm font-medium text-gray-900">
Create a new bookmark
</span>
</a>
@endforelse

Добавляем контроллера перехода по закладке

Теперь мы всё связали. Мы можем создать список и удалить. Последнее, что я сделаю, это добавлю способ просмотра закладок в представление. Простым способом сделать это было бы добавить кнопку, позволяющую открывать ссылку в новой вкладке, но это было бы скучно…

Вместо этого я буду перенаправлять закладку с реферером, что бы люди могли отслеживать, откуда она исходит. Для этого снова создадим контроллер с помощью консольной команды artisan:

php artisan make:controller Bookmarks/RedirectController --invokable

Добавим GET маршрут:

Route::get(
'bookmarks/{bookmark}',
App\Http\Controllers\Bookmarks\RedirectController::class
)->middleware(['auth'])->name('bookmarks.redirect');

Управление созданием этого URL-адреса мы могли бы написать вручную. Однако я создал библиотеку для таких ситуаций под названием juststeveking/uri-builder, которая позволит мне создавать URI и свободно добавлять дополнительные части.

declare(strict_types=1);
namespace App\Http\Controllers\Bookmarks;
use App\Http\Controllers\Controller;
use App\Models\Bookmark;
use Illuminate\Http\Request;
use JustSteveKing\UriBuilder\Uri;
class RedirectController extends Controller
{
public function __invoke(Request $request, Bookmark $bookmark)
{
$url = Uri::fromString(
uri: $bookmark->url,
)->addQueryParam(
key: 'utm_campaign',
value: 'bookmarker_' . auth()->id(),
)->addQueryParam(
key: 'utm_source',
value: 'Bookmarker App'
)->addQueryParam(
key: 'utm_medium',
value: 'website',
);
return redirect(
$url->toString(),
);
}
}

Вам не нужно углубляться так сильно, как это делаю я — это часть зависит от вас. Наконец, мы можем просто добавить ссылку в пользовательский интерфейс и всё готово.

<div class="flex items-center justify-between">
<p class="text-sm font-medium text-indigo-600 truncate">
{{ $bookmark->name }}
</p>
<div class="ml-2 flex-shrink-0 flex">
<a
href="{{ route('bookmarks.redirect', $bookmark->id) }}"
target="__blank"
rel="nofollow noopener"
class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-indigo-600 bg-gray-100 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>Visit</a>
<form method="POST" action="{{ route('bookmarks.delete', $bookmark->id) }}">
@csrf
@method('DELETE')
<button type="submit" class="ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-red-500 bg-gray-100 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Delete
</button>
</form>
</div>
</div>

Вы можете пойти дальше, собрав статистику о том, какие закладки вы просматривали больше всего, или что-то ещё. Вы даже можете позволить кликать по тэгам, что бы посмотреть все закладки связанные с данным тэгом, но я думаю этого будет достаточно для данного руководства, чтобы понять основы.


Как вам это руководство? Написание руководства для людей незнакомых с Laravel может быть сложной задачей, и я надеюсь, мне удалось объяснить основы достаточно хорошо и подробно, что бы вы могли следовать ему.

Дополнительные материалы

Предыдущая Статья

Laravel: Правило валидации с периодом дат и несколькими полями

Следующая Статья

JavaScript: Полное руководство по модулям в браузере и Node.js