Привет, Хабр. В этой статье я хочу поделиться своим опытом использования ресурсных контроллеров в CRUD приложении на фреймворке Laravel - простенькой CRM. Итак начнём.
Проект опубликован как свободное ПО.
Задача
Создать веб-приложение для учёта бизнес-клиентов: карточки организаций и их представителей, заявки, события, договора по принципу CRUD.
Ингредиенты
Будем использовать свободное программное обеспечение. В качестве PHP - фреймворка будем использовать Laravel и Bootstrap в качестве его компонента для построения страниц HTML. И поскольку это обычное CRUD приложение, то будем использовать ресурсные контроллеры для базовых действий: создание (create), чтение (read), модификация (update), удаление (delete).
Приступаем
Создаём проект Laravel, используя Composer:
composer create-project laravel/laravel crm.example.com
cd crm.example.com
Подключаем Bootstrap, делаем форму входа для пользователей и собираем через npm:
composer require laravel/ui
php artisan ui bootstrap
php artisan ui bootstrap --auth
npm install
npm run dev
Создаём модель для бизнес-клиентов, а также заодно к ней миграцию, фабрику, наполнитель, политику, контроллер и запрос формы:
php artisan make:model Client --all
Остальные модели также. Генерируем миграции для сессий и очередей:
php artisan session:table
php artisan queue:table
Создайте по необходимости в папке database/seeder
файлы для наполнения базы данных первичными данными, например для создания пользователя-админа:
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class UserSeeder extends Seeder
{
public function run()
{
DB::table('users')->insert([
[
'name' => 'admin',
'email' => 'admin@example.com',
'access_level' => 2,
'password' => Hash::make('твой_пароль')
]
]);
}
}
Настраиваем миграции. Таблица для клиентов будет выглядеть примерно так:
<?php
// ...
class CreateClientsTable extends Migration
{
public function up()
{
Schema::create('clients', function (Blueprint $table) {
$table->id();
$table->string('fulltitle', 512)->nullable();
$table->string('title', 128)->nullable();
/* ... тут много полей для всяких реквизитов ... */
$table->string('tel', 10)->nullable(); // Телефон
$table->string('comment', 255)->nullable(); // Комментарий
$table->foreignId('creater_id') // какой пользователь создал
->nullable()
->constrained('users')
->cascadeOnUpdate()
->nullOnDelete();
$table->foreignId('updater_id') // какой последний пользователь внёс изменения
->nullable()
->constrained('users')
->cascadeOnUpdate()
->nullOnDelete();
$table->timestamps();
});
}
public function down()
{
Schema::dropIfExists('clients');
}
}
После запускаем миграции с наполнением БД:
php artisan migrate:fresh --seed
:fresh - удалит существующие таблицы в БД.
Итак, в папке app/Http/Controllers
будет нас ждать ресурсный контроллер ClientController
. Там будут объявлены методы: index, create, store, show, edit, update, destroy.
Метод index должен отображать список клиентов. Напишем так, чтобы он выводил сначала новые записи, разбивая на страницы по 20 записей, используя для вывода Blade шаблон 'client-index':
<?php
// ...
public function index()
{
return view('client-index', ['clients' => Client::orderByDesc('id')->paginate(20)]);
}
Метод create должен вывести форму для заполнения данных о клиенте и кнопку для добавления его в БД. Мы используем Blade шаблон 'client-edit' и для добавления и для редактирования данных, чтобы шаблон понимал, какое именно из этих действий выполняется, мы будем передавать ему переменную edit , где значение 0 - для добавления нового, а 1 - для изменения существующего. Переменную client мы просто заполняем начальными данными из модели Client
<?php
// ...
public function create()
{
return view('client-edit', ['edit' => 0, 'client' => new Client()]);
}
Метод edit похож на метод create. Только здесь edit = 1, а переменная client заполняется моделью существующего клиента, переданного через маршрут:
<?php
// ...
public function edit(Client $client)
{
return view('client-edit', ['edit' => 1, 'client' => $client]);
}
Метод show отображает карточку клиента:
<?php
// ...
public function show(Client $client)
{
return view('client-show', ['client' => $client]);
}
Метод store добавляет нового клиента в БД, а метод update обновляет данные у существующего в БД:
<?php
// ...
public function store(StoreClientRequest $request)
{
$validated = $request->validated();
$client = Client::create($validated);
return redirect()->route('clients.show', $client);
}
public function update(UpdateClientRequest $request, Client $client)
{
$validated = $request->validated();
$client->fill($validated);
$client->updater_id = Auth::id();
$client->save();
return redirect()->route('clients.show', $client);
}
Метод destroy удаляет запись из БД:
<?php
// ...
public function destroy(Client $client)
{
if (Auth::user()->access_level == 2) {
$client->delete();
return redirect()->route('clients.index');
} else {
return null;
}
}
Теперь рассмотрим, что такое StoreClientRequest и UpdateClientRequest. Дело в том, что перед внесением записей в БД, мы должны проверить корректность данных и авторизовать действие пользователя, то есть пройти валидацию запроса. Они называются запросами формы и лежат в папке app/Http/Requests. Так будет примерно выглядеть StoreClientRequest, который проверяет данные нового клиента перед добавлением:
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreClientRequest extends FormRequest
{
*/
public function authorize()
{
return ($this->user()->access_level > 0); // имеет ли право пользоваель на это действие
}
public function rules()
{
return [
'fulltitle' => ['nullable', 'string', 'max:512'],
'title' => ['nullable', 'string', 'max:128'],
/* ... тут проверяется много полей для всяких реквизитов ... */
'tel' => ['nullable', 'string', 'max:10'],
'comment' => ['nullable', 'string', 'max:255']
];
}
}
UpdateClientRequest будет выглядеть примерно также.
Теперь настроим маршруты в routes/web.php:
<?php
// ...
// объявление маршрутов на входы пользователей, отключив маршруты регистрации, сброса пароля и верификации
Auth::routes(['register' => false, 'reset' => false,'verify' => false]);
// ставим посредник auth, чтобы только авторизованные пользователи могли ходить по этим маршрутам
Route::group(['middleware' => 'auth'], function() {
Route::resources([
'clients' => ClientController::class,
'clients.representatives' => ClientRepresentativeController::class,
/* тут ещё куча строк */
]);
});
Маршруты для методов ресурсного контроллера создаются автоматически Laravel, не надо для каждого метода прописывать отдельный маршрут. Для объявления маршрутов ресурсного контроллера мы используем Route::resources
, передавая массив ресурсных контроллеров. Так для клиентов Laravel создаст следующие маршруты:
clients.index
clients.show
clients.create
clients.edit
clients.store
clients.update
clients.destroy
Теперь о 'clients.representatives' => ClientRepresentativeController::class
, это объявление маршрутов к методам контроллера вложенных ресурсов. В нашей CRM ведётся учёт представителей бизнес-клиентов. Модель представителей является дочерней для модели клиентов. И чтобы мы могли увидеть, например всех представителей для конкретного клиента по адресу вроде https://crm.example.com/clients/123/representatives
, мы создадим контроллер вложенных ресурсов:
<?php
namespace App\Http\Controllers;
use App\Http\Requests\StoreRepresentativeRequest;
use App\Models\Representative;
use App\Models\Client;
class ClientRepresentativeController extends Controller
{
public function index(Client $client)
{
return view('representative-index', ['client' => $client, 'representatives' => Representative::where(['client_id' => $client->id])->orderByDesc('id')->paginate(20)]);
}
public function create(Client $client)
{
return view('representative-edit', ['edit' => 0, 'representative' => new Representative(['client_id' => $client->id])]);
}
public function store(StoreRepresentativeRequest $request, Client $client)
{
$validated = $request->validated();
$representative = new Representative(['client_id' => $client->id]);
$representative->fill($validated);
$representative->save();
return redirect()->route('representatives.show', $representative);
}
}
Метод index выведет список представителей для конкретного клиента. Метод create выведет форму для создания представителя, а также укажет для какого клиента он создаётся. Метод store сохранит запись о новом представителе с указанием id клиента.
А для остальных методов нам не нужен контроллер вложенных ресурсов, поскольку каждая существующая запись о представителях имеет уникальный идентификатор, и с ними мы можем взаимодействовать через обычный ресурсный контроллер RepresentativeController. А Laravel нам также создаст маршруты для методов контроллера вложенных ресурсов:
clients.representatives.index
clients.representatives.create
clients.representatives.store
Полностью посмотреть код, в том числе шаблоны Blade вы можете по ссылке.
xRange
Я бы порекомендовал:
Использовать политики для проверки прав, чтоб не писать костыли с if'ами. К тому не понятно почему формы создания/редактирования отображается для всех, а сохранять ее можно не всем.
Не понятно зачем нужно поле у клиентов updater_id, оно почти не носит никакой логики. Каждое последующее редактирование перетрет значение. Возможно лучше хранить отдельно логи: кто, когда и что изменил.
Создавать и редактировать вложенные модели лучше через их релейшены:
$client->representatives()->create($validated);
deyen Автор
Про 1 и 2: это простенькая CRM, которая создавалась для микроорганизации, и пользуются ей максимум 3 человека (директор (admin), секретарь(read-only user) и менеджер(editor)). Поэтому момент с доступом к отображению формы был опущен, главное чтобы данные сохранялись. В этом случае им было достаточно updater_id. А вообще да, с Вами согласен. Если нужно более сложное решение, то всё это надо делать.
Про 3: Про создание модели согласен. А в случае обновления модели используется уникальный id, и потому использовать родительскую модель не требуется.