Магазин на Laravel 7, часть 21. Добавляем профили и используем их при оформлении заказа
02.11.2020
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Пользователь • Практика • Фреймворк • ШаблонСайта
Нехорошо заставлять постоянного покупателя, который зарегистрирован на сайте, вводить одинаковые данные при каждом оформлении заказа. Давайте добавим возможность создания профилей в личном кабинете. Профиль содержит все данные, которые необходимы, чтобы оформить заказ в магазине, то есть имя и фамилию, адрес почты, номер телефона, адрес доставки. Тогда при оформлении заказа пользователь будет просто выбирать нужный профиль, вместо заполнения всех этих полей.
Добавляем профили
Создаем модель и миграцию с помощью artisan-команды:
> php artisan make:model Models/Profile -m
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateProfilesTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('profiles', function (Blueprint $table) { $table->id(); $table->bigInteger('user_id')->unsigned()->nullable(false); $table->string('title')->nullable(false); // название профиля $table->string('name')->nullable(false); // имя пользователя $table->string('email')->nullable(false); // почта пользователя $table->string('phone')->nullable(false); // телефон пользователя $table->string('address')->nullable(false); // адрес доставки заказа $table->string('comment')->nullable(); // комментарий к заказу $table->timestamps(); // внешний ключ, ссылается на поле id таблицы users $table->foreign('user_id') ->references('id') ->on('users') ->cascadeOnDelete(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('profiles'); } }
Запускаем миграцию, чтобы создать таблицу базы данных:
> php artisan migrate
Создаем ресурсный контроллер для CRUD-операций над профилями:
> php artisan make:controller ProfileController --resource --model=Models/Profile
Создадим маршртуты для CRUD-операций над профилями:
Route::name('user.')->prefix('user')->group(function () { // регистрация, вход в ЛК, восстановление пароля Auth::routes(); }); Route::group([ 'as' => 'user.', // имя маршрута, например user.index 'prefix' => 'user', // префикс маршрута, например user/index 'middleware' => ['auth'] // один или несколько посредников ], function () { // главная страница личного кабинета пользователя Route::get('index', 'UserController@index')->name('index'); // CRUD-операции над профилями пользователя Route::resource('profile', 'ProfileController'); });
Маршрут главной страницы личного кабинета теперь в другой группе. Эта группа использует посредник auth
, то есть все маршруты в ней доступны только аутентифицированным пользователям. А посредник в конструкторе контроллера UserController
уберем.
namespace App\Http\Controllers; class UserController extends Controller { public function __construct() { $this->middleware('auth'); } public function index() { return view('user.index'); } }
namespace App\Http\Controllers; class UserController extends Controller { public function index() { return view('user.index'); } }
Добавим в модель User
связи между таблицами:
class User extends Authenticatable { /** * Связь «один ко многим» таблицы `users` с таблицей `profiles` * * @return \Illuminate\Database\Eloquent\Relations\HasMany */ public function profiles() { return $this->hasMany(Profile::class); } }
Добавим в модель Profile
связи между таблицами:
class Profile extends Model { /** * Связь «профиль принадлежит» таблицы `profiles` с таблицей `users` * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function user() { return $this->belongsTo(User::class); } }
Теперь поработаем над контроллером ProfileController
:
namespace App\Http\Controllers; use App\Models\Profile; use Illuminate\Http\Request; class ProfileController extends Controller { /** * Показывает список всех профилей * * @return \Illuminate\Http\Response */ public function index() { $profiles = auth()->user()->profiles()->paginate(4); return view('user.profile.index', compact('profiles')); } /** * Показывает форму для создания профиля * * @return \Illuminate\Http\Response */ public function create() { return view('user.profile.create'); } /** * Сохраняет новый профиль в базу данных * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function store(Request $request) { // проверяем данные формы профиля $this->validate($request, [ 'user_id' => 'in:' . auth()->user()->id, 'title' => 'required|max:255', 'name' => 'required|max:255', 'email' => 'required|email|max:255', 'phone' => 'required|max:255', 'address' => 'required|max:255', ]); // валидация пройдена, создаем профиль $profile = Profile::create($request->all()); return redirect() ->route('user.profile.show', ['profile' => $profile->id]) ->with('success', 'Новый профиль успешно создан'); } /** * Показывает информацию о профиле * * @param \App\Models\Profile $profile * @return \Illuminate\Http\Response */ public function show(Profile $profile) { if ($profile->user_id !== auth()->user()->id) { abort(404); // это чужой профиль } return view('user.profile.show', compact('profile')); } /** * Показывает форму для редактирования профиля * * @param \App\Models\Profile $profile * @return \Illuminate\Http\Response */ public function edit(Profile $profile) { if ($profile->user_id !== auth()->user()->id) { abort(404); // это чужой профиль } return view('user.profile.edit', compact('profile')); } /** * Обновляет профиль (запись в таблице БД) * * @param \Illuminate\Http\Request $request * @param \App\Models\Profile $profile * @return \Illuminate\Http\Response */ public function update(Request $request, Profile $profile) { // проверяем данные формы профиля $this->validate($request, [ 'user_id' => 'in:' . auth()->user()->id, 'title' => 'required|max:255', 'name' => 'required|max:255', 'email' => 'required|email|max:255', 'phone' => 'required|max:255', 'address' => 'required|max:255', ]); // валидация пройдена, обновляем профиль $profile->update($request->all()); return redirect() ->route('user.profile.show', ['profile' => $profile->id]) ->with('success', 'Профиль был успешно отредактирован'); } /** * Удаляет профиль (запись в таблице БД) * * @param \App\Models\Profile $profile * @return \Illuminate\Http\Response */ public function destroy(Profile $profile) { if ($profile->user_id !== auth()->user()->id) { abort(404); // это чужой профиль } $profile->delete(); return redirect() ->route('user.profile.index') ->with('success', 'Профиль был успешно удален'); } }
Поскольку мы используем «mass assignment», добавляем свойство fillable
:
class Profile extends Model { protected $fillable = [ 'user_id', 'title', 'name', 'email', 'phone', 'address', 'comment', ]; /* ... */ }
Шаблон списка всех профилей пользователя views/user/profile/index.blade.php
:
@extends('layout.site', ['title' => 'Ваши профили']) @section('content') <h1>Ваши профили</h1> <a href="{{ route('user.profile.create') }}" class="btn btn-success mb-4"> Создать профиль </a> @if (count($profiles)) <table class="table table-bordered"> <tr> <th>№</th> <th width="22%">Наименование</th> <th width="22%">Имя, Фамилия</th> <th width="22%">Адрес почты</th> <th width="22%">Номер телефона</th> <th><i class="fas fa-edit"></i></th> <th><i class="fas fa-trash-alt"></i></th> </tr> @foreach($profiles as $profile) <tr> <td>{{ $loop->iteration }}</td> <td> <a href="{{ route('user.profile.show', ['profile' => $profile->id]) }}"> {{ $profile->title }} </a> </td> <td>{{ $profile->name }}</td> <td><a href="mailto:{{ $profile->email }}">{{ $profile->email }}</a></td> <td>{{ $profile->phone }}</td> <td> <a href="{{ route('user.profile.edit', ['profile' => $profile->id]) }}"> <i class="far fa-edit"></i> </a> </td> <td> <form action="{{ route('user.profile.destroy', ['profile' => $profile->id]) }}" method="post" onsubmit="return confirm('Удалить этот профиль?')"> @csrf @method('DELETE') <button type="submit" class="m-0 p-0 border-0 bg-transparent"> <i class="far fa-trash-alt text-danger"></i> </button> </form> </td> </tr> @endforeach </table> {{ $profiles->links() }} @endif @endsection
Шаблон просмотра профиля пользователя views/user/profile/show.blade.php
:
@extends('layout.site', ['title' => 'Данные профиля']) @section('content') <h1>Данные профиля</h1> <p><strong>Название профиля:</strong> {{ $profile->title }}</p> <p><strong>Имя, фамилия:</strong> {{ $profile->name }}</p> <p> <strong>Адрес почты:</strong> <a href="mailto:{{ $profile->email }}">{{ $profile->email }}</a> </p> <p><strong>Номер телефона:</strong> {{ $profile->phone }}</p> <p><strong>Адрес доставки:</strong> {{ $profile->address }}</p> @isset ($profile->comment) <p><strong>Комментарий:</strong> {{ $profile->comment }}</p> @endisset <a href="{{ route('user.profile.edit', ['profile' => $profile->id]) }}" class="btn btn-success"> Редактировать профиль </a> <form method="post" class="d-inline" onsubmit="return confirm('Удалить этот профиль?')" action="{{ route('user.profile.destroy', ['profile' => $profile->id]) }}"> @csrf @method('DELETE') <button type="submit" class="btn btn-danger"> Удалить профиль </button> </form> @endsection
Шаблон создания профиля пользователя views/user/profile/create.blade.php
:
@extends('layout.admin', ['title' => 'Создание профиля']) @section('content') <h1>Создание профиля</h1> <form method="post" action="{{ route('user.profile.store') }}"> @include('user.profile.part.form') </form> @endsection
Шаблон редактирования профиля пользователя views/user/profile/edit.blade.php
:
@extends('layout.site', ['title' => 'Редактирование профиля']) @section('content') <h1>Редактирование профиля</h1> <form method="post" action="{{ route('user.profile.update', ['profile' => $profile->id]) }}"> @method('PUT') @include('user.profile.part.form') </form> @endsection
Вспомогательный шаблон с формой создания-редактирования views/user/profile/part/form.blade.php
:
@csrf <input type="hidden" name="user_id" value="{{ auth()->user()->id }}"> <div class="form-group"> <input type="text" class="form-control" name="title" placeholder="Название профиля" required maxlength="255" value="{{ old('title') ?? $profile->title ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Имя, Фамилия" required maxlength="255" value="{{ old('name') ?? $profile->name ?? '' }}"> </div> <div class="form-group"> <input type="email" class="form-control" name="email" placeholder="Адрес почты" required maxlength="255" value="{{ old('email') ?? $profile->email ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="phone" placeholder="Номер телефона" required maxlength="255" value="{{ old('phone') ?? $profile->phone ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="address" placeholder="Адрес доставки" required maxlength="255" value="{{ old('address') ?? $profile->address ?? '' }}"> </div> <div class="form-group"> <textarea class="form-control" name="comment" placeholder="Комментарий" maxlength="255" rows="2">{{ old('comment') ?? $profile->comment ?? '' }}</textarea> </div> <div class="form-group"> <button type="submit" class="btn btn-success">Сохранить</button> </div>
Используем профили
Теперь при оформлении заказа нам нужно предоставить аутентифицированному пользователю возможность выбрать подходящий профиль. Начнем с метода checkout()
контроллера BasketController
.
class BasketController extends Controller { /** * Форма оформления заказа * * @param Request $request * @return \Illuminate\Http\Response */ public function checkout(Request $request) { $profile = null; $profiles = null; if (auth()->check()) { // если пользователь аутентифицирован $user = auth()->user(); // ...и у него есть профили для оформления $profiles = $user->profiles; // ...и был запрошен профиль для оформления $prof_id = (int)$request->input('profile_id'); if ($prof_id) { $profile = $user->profiles()->whereIdAndUserId($prof_id, $user->id)->first(); } } return view('basket.checkout', compact('profiles', 'profile')); } }
Если пользователь залогинен, мы получаем его профили и передаем в шаблон формы оформления заказа. На переменную $profile
пока не обращаем внимание, о ней чуть позже.
@extends('layout.site', ['title' => 'Оформить заказ']) @section('content') <h1 class="mb-4">Оформить заказ</h1> @if ($profiles && $profiles->count()) @include('basket.select', ['current' => $profile->id ?? 0]) @endif <form method="post" action="{{ route('basket.saveorder') }}" id="checkout"> <!-- ..... --> </form> @endsection
Как видите, в форме подключаем еще один шаблон views/basket/select.blade.php
, который позволяет выбрать подходящий профиль.
<form action="{{ route('basket.checkout') }}" method="get" id="profiles"> <div class="form-group"> <select name="profile_id" class="form-control"> <option value="0">Выберите профиль</option> @foreach($profiles as $profile) <option value="{{ $profile->id }}"@if($profile->id == $current) selected @endif> {{ $profile->title }} </option> @endforeach </select> </div> <div class="form-group"> <button type="submit" class="btn btn-primary">Выбрать</button> </div> </form>
При отправке этой формы передается GET-параметр profile_id
, данные отправляются на роут basket.checkout
. За показ страницы по этому роуту отвечает метод checkout()
контроллера. Показывается все та же форма оформления заказа, но мы уже можем получить данные профиля, поскольку есть идентификатор $profile_id
. И передать данные профиля в шаблон, чтобы заполнить поля формы.
@extends('layout.site', ['title' => 'Оформить заказ']) @section('content') <h1 class="mb-4">Оформить заказ</h1> @isset ($profiles) @include('basket.select', ['current' => $profile->id ?? 0]) @endisset <form method="post" action="{{ route('basket.saveorder') }}" id="checkout"> @csrf <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Имя, Фамилия" required maxlength="255" value="{{ old('name') ?? $profile->name ?? '' }}"> </div> <div class="form-group"> <input type="email" class="form-control" name="email" placeholder="Адрес почты" required maxlength="255" value="{{ old('email') ?? $profile->email ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="phone" placeholder="Номер телефона" required maxlength="255" value="{{ old('phone') ?? $profile->phone ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="address" placeholder="Адрес доставки" required maxlength="255" value="{{ old('address') ?? $profile->address ?? '' }}"> </div> <div class="form-group"> <textarea class="form-control" name="comment" placeholder="Комментарий" maxlength="255" rows="2">{{ old('comment') ?? $profile->comment ?? '' }}</textarea> </div> <div class="form-group"> <button type="submit" class="btn btn-success">Оформить</button> </div> </form> @endsection
Практически все готово, только давайте будем получать данные профиля с помощью ajax-запроса:
jQuery(document).ready(function($) { /* * Общие настройки ajax-запросов, отправка на сервер csrf-токена */ $.ajaxSetup({ headers: { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); /* * Раскрытие и скрытие пунктов меню каталога в левой колонке */ $('#catalog-sidebar > ul ul').hide(); $('#catalog-sidebar .badge').on('click', function () { /* ... */ }); /* * Получение данных профиля пользователя при оформлении заказа */ $('form#profiles button[type="submit"]').hide(); // при выборе профиля отправляем ajax-запрос, чтобы получить данные $('form#profiles select').change(function () { // если выбран элемент «Выберите профиль» if ($(this).val() == 0) { // очищаем все поля формы оформления заказа $('#checkout').trigger('reset'); return; } var data = new FormData($('form#profiles')[0]); $.ajax({ url: '/basket/profile', data: data, processData: false, contentType: false, type: 'POST', dataType: 'JSON', success: function(data) { $('input[name="name"]').val(data.profile.name); $('input[name="email"]').val(data.profile.email); $('input[name="phone"]').val(data.profile.phone); $('input[name="address"]').val(data.profile.address); $('textarea[name="comment"]').val(data.profile.comment); }, error: function (reject) { alert(reject.responseJSON.error); } }); }); });
Данные мы отправляем методом POST по маршруту basket/profile
. Соответственно, надо добавить маршрут:
Route::post('/basket/profile', 'BasketController@profile') ->name('basket.profile');
И добавляем метод profile()
в контроллер BasketController
, который будет возвращать данные профиля в json-формате.
class BasketController extends Controller { /** * Возвращает профиль пользователя в формате JSON * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response */ public function profile(Request $request) { if ( ! $request->ajax()) { abort(404); } if ( ! auth()->check()) { return response()->json(['error' => 'Нужна авторизация!'], 404); } $user = auth()->user(); $profile_id = (int)$request->input('profile_id'); if ($profile_id) { $profile = $user->profiles()->whereIdAndUserId($profile_id, $user->id)->first(); if ($profile) { return response()->json(['profile' => $profile]); } } return response()->json(['error' => 'Профиль не найден!'], 404); } }
И последнее — добавляем в layout-шаблон мета-тег c csrf-токеном, чтобы использовать в ajax-запросах:
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <meta name="csrf-token" content="{{ csrf_token() }}"> <title>{{ $title ?? 'Интернет-магазин' }}</title> <link rel="stylesheet" href="{{ asset('css/app.css') }}"> <link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css"/> <link rel="stylesheet" href="{{ asset('css/site.css') }}"> <script src="{{ asset('js/app.js') }}"></script> <script src="{{ asset('js/site.js') }}"></script> </head>
- Магазин на Laravel 7, часть 8. Регистрация и аутентификация пользователей на сайте
- Магазин на Laravel 7, часть 24. Фильтр товаров категории по цене, новинкам и лидерам продаж
- Магазин на Laravel 7, часть 23. Главная страница сайта, новинки, лидеры продаж и распродажа
- Магазин на Laravel 7, часть 20. Показ отдельной страницы и верхнее меню всех страниц
- Магазин на Laravel 7, часть 18. Панель управления, пользователи и CRUD страниц сайта
- Магазин на Laravel 7, часть 17. Панель управления, работа с заказами, изменение статуса
- Магазин на Laravel 7, часть 16. Панель управления, CRUD-операции для товаров каталога
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Пользователь • Практика • Фреймворк • Шаблон сайта