Магазин на Laravel 7, часть 10. Форма оформления, сохранение заказа в базу данных
11.10.2020
Теги: Laravel • MySQL • PHP • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Миграции • Практика • Форма • Фреймворк • ШаблонСайта
Давайте теперь займемся оформлением заказа в магазине. Нам потребуются две таблицы в базе данных — таблица orders
(для хранения заказов) и таблица order_items
(для хранения заказанных товаров). Создаем две модели и две миграции с помощью artisan-команды.
> php artisan make:model Models/Order -m > php artisan make:model Models/OrderItem -m
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateOrdersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('orders', function (Blueprint $table) { $table->id(); $table->bigInteger('user_id')->unsigned()->nullable(); $table->string('name'); $table->string('email'); $table->string('phone'); $table->string('address'); $table->string('comment')->nullable(); $table->decimal('amount', 10, 2)->unsigned(); $table->timestamps(); // внешний ключ, ссылается на поле id таблицы users $table->foreign('user_id') ->references('id') ->on('users') ->nullOnDelete(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('orders'); } }
use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateOrderItemsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('order_items', function (Blueprint $table) { $table->id(); $table->bigInteger('order_id')->unsigned(); $table->bigInteger('product_id')->unsigned()->nullable(); $table->string('name', 100); $table->decimal('price', 10, 2)->unsigned(); $table->tinyInteger('quantity')->unsigned()->default(1); $table->decimal('cost', 10, 2)->unsigned(); // внешний ключ, ссылается на поле id таблицы orders $table->foreign('order_id') ->references('id') ->on('orders') ->cascadeOnDelete(); // внешний ключ, ссылается на поле id таблицы products $table->foreign('product_id') ->references('id') ->on('products') ->nullOnDelete(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('order_items'); } }
Запускаем миграцию, чтобы создать таблицы базы данных:
> php artisan migrate Migrating: 2020_10_10_105603_create_orders_table Migrated: 2020_10_10_105603_create_orders_table (0.06 seconds) Migrating: 2020_10_10_111729_create_order_items_table Migrated: 2020_10_10_111729_create_order_items_table (0.09 seconds)
Обратите внимание, что файлы классов моделей Order.php
и OrderItem.php
будут созданы в директории app/Models
. Это для того, чтобы не сваливать все модели в одну кучу в директории app
. И раз у нас теперь есть директория Models
— переместим в нее все файлы моделей из директории app
.
Надо изменить namespace
в файлах моделей User.php
, Product.php
, Category.php
, Brand.php
, Basket.php
, Auth/RegisterController
.
// вот так было namespace App;
// вот так стало namespace App\Models;
Поскольку в контроллерах мы используем модели — в них тоже потребуется внести изменения:
// вот так было use App\User; use App\Product; use App\Category; use App\Brand; use App\Basket;
// вот так стало use App\Models\User; use App\Models\Product; use App\Models\Category; use App\Models\Brand; use App\Models\Basket;
Еще два места, где нужно внести изменения — сервис-провайдер ComposerServiceProvider
и файл конфигурации config/auth.php
:
use App\Models\Basket; use App\Models\Brand; use App\Models\Category;
'providers' => [ 'users' => [ 'driver' => 'eloquent', 'model' => App\Models\User::class, ], ]
После этого почистим все, что Laravel успел закешировать:
> php artisan cache:clear # Очистка кэша приложения > php artisan route:clear # Очистка кэша маршрутов > php artisan view:clear # Очистка кэша шаблонов > php artisan config:clear # Очистка кэша конфигурации
При использовании IDE-helper-а «barryvdh/laravel-ide-helper» — заново создаем файл файл _ide_helper.php
в корне проекта:
> php artisan ide-helper:generate
Теперь настроим связи между моделями:
namespace App\Models; use Illuminate\Database\Eloquent\Model; class Order extends Model { /** * Связь «один ко многим» таблицы `orders` с таблицей `order_items` */ public function items() { return $this->hasMany(OrderItem::class); } }
namespace App\Models; use App\Models\Product; use Illuminate\Database\Eloquent\Model; class OrderItem extends Model { /** * Связь «элемент принадлежит» таблицы `order_items` с таблицей `products` */ public function product() { return $this->belongsTo(Product::class); } }
Файл шабона для оформления заказа у нас уже есть — это checkout.blade.php
в директории views/basket
. Но он практически пустой — так что разместим в нем форму с полями «Имя», «Почта», «Телефон», «Адрес» и «Комментарий».
@extends('layout.site') @section('content') <h1 class="mb-4">Оформить заказ</h1> <form method="post" action="{{ route('basket.saveorder') }}"> @csrf <div class="form-group"> <input type="text" class="form-control" name="name" placeholder="Имя, Фамилия" required maxlength="255" value="{{ old('name') ?? '' }}"> </div> <div class="form-group"> <input type="email" class="form-control" name="email" placeholder="Адрес почты" required maxlength="255" value="{{ old('email') ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="phone" placeholder="Номер телефона" required maxlength="255" value="{{ old('phone') ?? '' }}"> </div> <div class="form-group"> <input type="text" class="form-control" name="address" placeholder="Адрес доставки" required maxlength="255" value="{{ old('address') ?? '' }}"> </div> <div class="form-group"> <textarea class="form-control" name="comment" placeholder="Комментарий" maxlength="255" rows="2">{{ old('comment') ?? '' }}</textarea> </div> <div class="form-group"> <button type="submit" class="btn btn-success">Оформить</button> </div> </form> @endsection
Добавим маршрут, куда будем отправлять данные формы, чтобы сохранить заказ:
Route::post('/basket/saveorder', 'BasketController@saveOrder')->name('basket.saveorder');
Теперь добавим в контроллер BasketController
метод saveOrder()
:
class BasketController extends Controller { /** * Сохранение заказа в БД */ public function saveOrder(Request $request) { // проверяем данные формы оформления $this->validate($request, [ 'name' => 'required|max:255', 'email' => 'required|email|max:255', 'phone' => 'required|max:255', 'address' => 'required|max:255', ]); // валидация пройдена, сохраняем заказ $basket = Basket::getBasket(); $user_id = auth()->check() ? auth()->user()->id : null; $order = Order::create( $request->all() + ['amount' => $basket->getAmount(), 'user_id' => $user_id] ); foreach ($basket->products as $product) { $order->items()->create([ 'product_id' => $product->id, 'name' => $product->name, 'price' => $product->price, 'quantity' => $product->pivot->quantity, 'cost' => $product->price * $product->pivot->quantity, ]); } // уничтожаем корзину $basket->delete(); return redirect() ->route('basket.success') ->with('success', 'Ваш заказ успешно размещен'); } }
Мы здесь используем «Mass Assignment», так что в моделях Order
и OrderItem
должны указать, какие поля допускается передавать в метод create()
— иначе мы вообще не сможем сохранить данные заказа в базу данных.
class Order extends Model { protected $fillable = [ 'user_id', 'name', 'email', 'phone', 'address', 'comment', 'amount', ]; /* ... */ }
class OrderItem extends Model { protected $fillable = [ 'product_id', 'name', 'price', 'quantity', 'cost', ]; /* ... */ }
Кроме того, нам потребуется метод getAmount()
в модели Basket
. Этот метод следовало бы создать еще раньше, чтобы не рассчитывать стоимость корзины в шаблоне. Но лучше поздно, чем никогда.
class Basket extends Model { /* ... */ public function getAmount() { $amount = 0.0; foreach ($this->products as $product) { $amount = $amount + $product->price * $product->pivot->quantity; } return $amount; } /* ... */ }
После отправки формы у меня появилась такая ошибка:
Illuminate\Database\QueryException
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'updated_at' in 'field list'
(SQL: insert into `order_items` (`name`, `price`, `quantity`, `cost`, `order_id`, `updated_at`, `created_at`)
values ('Какое-то название товара каталога', 1412.00, 2, 2824, 1, 2020-10-11 08:14:21, 2020-10-11 08:14:21))
По умолчанию Laravel ожидает столбцы created_at
и updated_at
в таблице order_items
. Давайте сообщим фреймворку, что таких полей нет — для этого добавим в модель OrderItem
свойство $timestamps
.
class OrderItem extends Model { public $timestamps = false; /* ... */ }
Если при заполнении формы покупатель допустил ошибки — ему снова будет показана форма. Причем, форма уже будет заполнена введенными данными — потому что мы использовали хелпер old()
. Но мы не показываем сообщения об ошибках, которые были допущены — непонятно, что исправлять. Так что отредактируем layout-шаблон site.blade.php
.
<div class="col-md-9"> @if ($message = Session::get('success')) <div class="alert alert-success alert-dismissible mt-0" role="alert"> <button type="button" class="close" data-dismiss="alert" aria-label="Закрыть"> <span aria-hidden="true">×</span> </button> {{ $message }} </div> @endif @if ($errors->any()) <div class="alert alert-danger alert-dismissible mt-0" role="alert"> <button type="button" class="close" data-dismiss="alert" aria-label="Закрыть"> <span aria-hidden="true">×</span> </button> <ul class="mb-0"> @foreach ($errors->all() as $error) <li>{{ $error }}</li> @endforeach </ul> </div> @endif @yield('content') </div>
Не слишком удачно, что после успешного размещения заказа покупатель попадает на страницу пустой корзины. В принципе, flash-сообщение «Ваш заказ успешно размещен» сообщает, что все прошло успешно. Но лучше, если мы отправим покупателя на отдельную страницу, где покажем состав и данные заказа.
Route::get('/basket/success', 'BasketController@success') ->name('basket.success');
class BasketController extends Controller { /** * Сохранение заказа в БД */ public function saveOrder(Request $request) { /* ... */ return redirect() ->route('basket.success') ->with('order_id', $order->id); } /** * Сообщение об успешном оформлении заказа */ public function success(Request $request) { if ($request->session()->exists('order_id')) { // сюда покупатель попадает сразу после успешного оформления заказа $order_id = $request->session()->pull('order_id'); $order = Order::findOrFail($order_id); return view('basket.success', compact('order')); } else { // если покупатель попал сюда случайно, не после оформления заказа, // ему здесь делать нечего — отправляем на страницу корзины return redirect()->route('basket.index'); } } }
Шаблон view/basket/success.blade.php
страницы, куда попадает покупатель сразу после размещения заказа:
@extends('layout.site') @section('content') <h1>Заказ размещен</h1> <p>Ваш заказ успешно размещен. Наш менеджер скоро свяжется с Вами для уточнения деталей.</p> <h2>Ваш заказ</h2> <table class="table table-bordered"> <tr> <th>№</th> <th>Наименование</th> <th>Цена</th> <th>Кол-во</th> <th>Стоимость</th> </tr> @foreach($order->items as $item) <tr> <td>{{ $loop->iteration }}</td> <td>{{ $item->name }}</td> <td>{{ number_format($item->price, 2, '.', '') }}</td> <td>{{ $item->quantity }}</td> <td>{{ number_format($item->cost, 2, '.', '') }}</td> </tr> @endforeach <tr> <th colspan="4" class="text-right">Итого</th> <th>{{ number_format($order->amount, 2, '.', '') }}</th> </tr> </table> <h2>Ваши данные</h2> <p>Имя, фамилия: {{ $order->name }}</p> <p>Адрес почты: <a href="mailto:{{ $order->email }}">{{ $order->email }}</a></p> <p>Номер телефона: {{ $order->phone }}</p> <p>Адрес доставки: {{ $order->address }}</p> @isset ($order->comment) <p>Комментарий: {{ $order->comment }}</p> @endisset @endsection
- Магазин на Laravel 7, часть 24. Фильтр товаров категории по цене, новинкам и лидерам продаж
- Магазин на Laravel 7, часть 13. Панель управления, обрезка изображения и валидация данных
- Магазин на Laravel 7, часть 12. Панель управления, создание и редактирование категорий
- Магазин на Laravel 7, часть 6. Изменение количества товара, удаление товара из корзины
- Магазин на Laravel 7, часть 25. Поиск по каталогу товаров, деплой проекта на хостинг TimeWeb
- Магазин на Laravel 7, часть 23. Главная страница сайта, новинки, лидеры продаж и распродажа
- Магазин на Laravel 7, часть 22. Рефакторинг кода, работа над каталогом товаров и корзиной
Поиск: Laravel • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Миграции • Форма • Фреймворк • Шаблон сайта • Практика