Привет, Хабр. В этой статье я хочу поделиться своим опытом использования ресурсных контроллеров в 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 вы можете по ссылке.

Комментарии (2)


  1. xRange
    19.10.2022 19:45
    +1

    Я бы порекомендовал:

    1. Использовать политики для проверки прав, чтоб не писать костыли с if'ами. К тому не понятно почему формы создания/редактирования отображается для всех, а сохранять ее можно не всем.

    2. Не понятно зачем нужно поле у клиентов updater_id, оно почти не носит никакой логики. Каждое последующее редактирование перетрет значение. Возможно лучше хранить отдельно логи: кто, когда и что изменил.

    3. Создавать и редактировать вложенные модели лучше через их релейшены: $client->representatives()->create($validated);


    1. deyen Автор
      20.10.2022 07:40

      Про 1 и 2: это простенькая CRM, которая создавалась для микроорганизации, и пользуются ей максимум 3 человека (директор (admin), секретарь(read-only user) и менеджер(editor)). Поэтому момент с доступом к отображению формы был опущен, главное чтобы данные сохранялись. В этом случае им было достаточно updater_id. А вообще да, с Вами согласен. Если нужно более сложное решение, то всё это надо делать.

      Про 3: Про создание модели согласен. А в случае обновления модели используется уникальный id, и потому использовать родительскую модель не требуется.