Магазин на Laravel 7, часть 10. Форма оформления, сохранение заказа в базу данных

11.10.2020

Теги: LaravelMySQLPHPWeb-разработкаБазаДанныхИнтернетМагазинКаталогТоваровКорзинаМиграцииПрактикаФормаФреймворкШаблонСайта

Давайте теперь займемся оформлением заказа в магазине. Нам потребуются две таблицы в базе данных — таблица 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">&times;</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">&times;</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 • MySQL • PHP • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Миграции • Форма • Фреймворк • Шаблон сайта • Практика

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.