Магазин на Laravel 7, часть 6. Изменение количества товара, удаление товара из корзины

04.10.2020

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

Обновление корзины

Для каждого товара в корзине есть две кнопки — «Плюс» и «Минус», которые увеличивают или уменьшают количество. Давайте добавим два маршрута, создадим две формы в шаблоне и реализуем два метода в контроллере — plus() и minus().

Route::post('/basket/plus/{id}', 'BasketController@plus')
    ->where('id', '[0-9]+')
    ->name('basket.plus');
Route::post('/basket/minus/{id}', 'BasketController@minus')
    ->where('id', '[0-9]+')
    ->name('basket.minus');
<td>
    <form action="{{ route('basket.minus', ['id' => $product->id]) }}"
          method="post" class="d-inline">
        @csrf
        <button type="submit" class="m-0 p-0 border-0 bg-transparent">
            <i class="fas fa-minus-square"></i>
        </button>
    </form>
    <span class="mx-1">{{ $itemQuantity }}</span>
    <form action="{{ route('basket.plus', ['id' => $product->id]) }}"
          method="post" class="d-inline">
        @csrf
        <button type="submit" class="m-0 p-0 border-0 bg-transparent">
            <i class="fas fa-plus-square"></i>
        </button>
    </form>
</td>
class BasketController extends Controller {
    /**
     * Увеличивает кол-во товара $id в корзине на единицу
     */
    public function plus(Request $request, $id) {
        $basket_id = $request->cookie('basket_id');
        if (empty($basket_id)) {
            abort(404);
        }
        $this->change($basket_id, $id, 1);
        // выполняем редирект обратно на страницу корзины
        return redirect()
            ->route('basket.index')
            ->withCookie(cookie('basket_id', $basket_id, 525600));
    }

    /**
     * Уменьшает кол-во товара $id в корзине на единицу
     */
    public function minus(Request $request, $id) {
        $basket_id = $request->cookie('basket_id');
        if (empty($basket_id)) {
            abort(404);
        }
        $this->change($basket_id, $id, -1);
        // выполняем редирект обратно на страницу корзины
        return redirect()
            ->route('basket.index')
            ->withCookie(cookie('basket_id', $basket_id, 525600));
    }

    /**
     * Изменяет кол-во товара $product_id на величину $count
     */
    private function change($basket_id, $product_id, $count = 0) {
        if ($count == 0) {
            return;
        }
        $basket = Basket::findOrFail($basket_id);
        // если товар есть в корзине — изменяем кол-во
        if ($basket->products->contains($product_id)) {
            $pivotRow = $basket->products()->where('product_id', $product_id)->first()->pivot;
            $quantity = $pivotRow->quantity + $count;
            if ($quantity > 0) {
                // обновляем кол-во товара $product_id в корзине
                $pivotRow->update(['quantity' => $quantity]);
                // обновляем поле `updated_at` таблицы `baskets`
                $basket->touch();
            } else {
                // кол-во равно нулю — удаляем товар из корзины
                $pivotRow->delete();
            }
        }
    }
}

Временна́я зона

Немного неудобно, что в базе данных поля created_at и updated_at сохраняются в UTC. Это можно изменить в настройках приложения, в файле config/app.php. Или в методе boot() сервис-провайдера App\Providers\AppServiceProvide.

return [
    /* ... */
    'timezone' => 'Europe/Moscow',
    /* ... */
];
class AppServiceProvider extends ServiceProvider {
    /* ... */
    public function boot(){
        date_default_timezone_set('Europe/Moscow');
    }
    /* ... */
}

Но мы этого делать не будем. В базе данных дату и время лучше хранить именно в UTC. А вот пользователю можно показывать дату и время с учетом временной зоны. Для этого надо либо спросить у пользователя временную зону (если он зарегистрирован) и сохранить в базу данных. Либо определить зону без участия пользователя, по ip-адресу. И при показе даты и времени на сайте, преобразовывать дату-время из БД с помощью аксессоров и мутаторов. Как это сделать с помощью пакета laravel-geoip — хорошо описано здесь.

Задействуем модель

У меня опять получился большой и запутанный контроллер, а модель не используется вовсе. Давайте это исправим и реализуем методы модели, которые позволят добавлять и удалять товар из корзины.

<?php
namespace App;

use Illuminate\Database\Eloquent\Model;

class Basket extends Model {
    /**
     * Связь «многие ко многим» таблицы `baskets` с таблицей `products`
     */
    public function products() {
        return $this->belongsToMany(Product::class)->withPivot('quantity');
    }

    /**
     * Увеличивает кол-во товара $id в корзине на величину $count
     */
    public function increase($id, $count = 1) {
        $this->change($id, $count);
    }

    /**
     * Уменьшает кол-во товара $id в корзине на величину $count
     */
    public function decrease($id, $count = 1) {
        $this->change($id, -1 * $count);
    }

    /**
     * Изменяет количество товара $id в корзине на величину $count;
     * если товара еще нет в корзине — добавляет этот товар; $count
     * может быть как положительным, так и отрицательным числом
     */
    private function change($id, $count = 0) {
        if ($count == 0) {
            return;
        }
        // если товар есть в корзине — изменяем кол-во
        if ($this->products->contains($id)) {
            // получаем объект строки таблицы `basket_product`
            $pivotRow = $this->products()->where('product_id', $id)->first()->pivot;
            $quantity = $pivotRow->quantity + $count;
            if ($quantity > 0) {
                // обновляем количество товара $id в корзине
                $pivotRow->update(['quantity' => $quantity]);
            } else {
                // кол-во равно нулю — удаляем товар из корзины
                $pivotRow->delete();
            }
        } elseif ($count > 0) { // иначе — добавляем этот товар
            $this->products()->attach($id, ['quantity' => $count]);
        }
        // обновляем поле `updated_at` таблицы `baskets`
        $this->touch();
    }

    /**
     * Удаляет товар с идентификатором $id из корзины покупателя
     */
    public function remove($id) {
        // удаляем товар из корзины (разрушаем связь)
        $this->products()->detach($id);
        // обновляем поле `updated_at` таблицы `baskets`
        $this->touch();
    }
}

Модель переделали, теперь надо удалить все лишнее из контроллера:

<?php
namespace App\Http\Controllers;

use App\Basket;
use Illuminate\Http\Request;
use Illuminate\Database\Eloquent\ModelNotFoundException;

class BasketController extends Controller {

    private $basket;

    public function __construct() {
        $this->getBasket();
    }

    /**
     * Показывает корзину покупателя
     */
    public function index() {
        $products = $this->basket->products;
        return view('basket.index', compact('products'));
    }

    /**
     * Форма оформления заказа
     */
    public function checkout() {
        return view('basket.checkout');
    }

    /**
     * Добавляет товар с идентификатором $id в корзину
     */
    public function add(Request $request, $id) {
        $quantity = $request->input('quantity') ?? 1;
        $this->basket->increase($id, $quantity);
        // выполняем редирект обратно на ту страницу,
        // где была нажата кнопка «В корзину»
        return back();
    }

    /**
     * Увеличивает кол-во товара $id в корзине на единицу
     */
    public function plus($id) {
        $this->basket->increase($id);
        // выполняем редирект обратно на страницу корзины
        return redirect()->route('basket.index');
    }

    /**
     * Уменьшает кол-во товара $id в корзине на единицу
     */
    public function minus($id) {
        $this->basket->decrease($id);
        // выполняем редирект обратно на страницу корзины
        return redirect()->route('basket.index');
    }

    /**
     * Возвращает объект корзины; если не найден — создает новый
     */
    private function getBasket() {
        $basket_id = request()->cookie('basket_id');
        if (!empty($basket_id)) {
            try {
                $this->basket = Basket::findOrFail($basket_id);
            } catch (ModelNotFoundException $e) {
                $this->basket = Basket::create();
            }
        } else {
            $this->basket = Basket::create();
        }
        Cookie::queue('basket_id', $this->basket->id, 525600);
    }
}

Удаление из корзины

Теперь в модели есть метод remove(), который позволяет удалить товар из корзины. Добавим два новых маршрута, несколько форм в шаблон страницы корзины и реализуем два метода в контроллере — remove() и clear().

Route::post('/basket/remove/{id}', 'BasketController@remove')
    ->where('id', '[0-9]+')
    ->name('basket.remove');
Route::post('/basket/clear', 'BasketController@clear')->name('basket.clear');
@extends('layout.site')

@section('content')
    <h1>Ваша корзина</h1>
    @if (count($products))
        @php
            $basketCost = 0;
        @endphp
        <form action="{{ route('basket.clear') }}" method="post" class="text-right">
            @csrf
            <button type="submit" class="btn btn-outline-danger mb-4 mt-0">
                Очистить корзину
            </button>
        </form>
        <table class="table table-bordered">
            <tr>
                <th></th>
                <th>Наименование</th>
                <th>Цена</th>
                <th>Кол-во</th>
                <th>Стоимость</th>
                <th></th>
            </tr>
            @foreach($products as $product)
                @php
                    $itemPrice = $product->price;
                    $itemQuantity =  $product->pivot->quantity;
                    $itemCost = $itemPrice * $itemQuantity;
                    $basketCost = $basketCost + $itemCost;
                @endphp
                <tr>
                    <td>{{ $loop->iteration }}</td>
                    <td>
                        <a href="{{ route('catalog.product', [$product->slug]) }}">
                            {{ $product->name }}
                        </a>
                    </td>
                    <td>{{ number_format($itemPrice, 2, '.', '') }}</td>
                    <td>
                        <form action="{{ route('basket.minus', ['id' => $product->id]) }}"
                              method="post" class="d-inline">
                            @csrf
                            <button type="submit" class="m-0 p-0 border-0 bg-transparent">
                                <i class="fas fa-minus-square"></i>
                            </button>
                        </form>
                        <span class="mx-1">{{ $itemQuantity }}</span>
                        <form action="{{ route('basket.plus', ['id' => $product->id]) }}"
                              method="post" class="d-inline">
                            @csrf
                            <button type="submit" class="m-0 p-0 border-0 bg-transparent">
                                <i class="fas fa-plus-square"></i>
                            </button>
                        </form>
                    </td>
                    <td>{{ number_format($itemCost, 2, '.', '') }}</td>
                    <td>
                        <form action="{{ route('basket.remove', ['id' => $product->id]) }}"
                              method="post">
                            @csrf
                            <button type="submit" class="m-0 p-0 border-0 bg-transparent">
                                <i class="fas fa-trash-alt text-danger"></i>
                            </button>
                        </form>
                    </td>
                </tr>
            @endforeach
            <tr>
                <th colspan="4" class="text-right">Итого</th>
                <th>{{ number_format($basketCost, 2, '.', '') }}</th>
                <th></th>
            </tr>
        </table>
    @else
        <p>Ваша корзина пуста</p>
    @endif
@endsection
class BasketController extends Controller {
    /**
     * Удаляет товар с идентификаторм $id из корзины
     */
    public function remove($id) {
        $this->basket->remove($id);
        // выполняем редирект обратно на страницу корзины
        return redirect()->route('basket.index');
    }

    /**
     * Полностью очищает содержимое корзины покупателя
     */
    public function clear() {
        $this->basket->delete();
        // выполняем редирект обратно на страницу корзины
        return redirect()->route('basket.index');
    }
}

Исправляем шаблоны

В layout-шаблоне надо добавить ссылку на страницу корзины. И нужна кнопка «Добавить в корзину» для списка товаров.

<nav class="navbar navbar-expand-lg navbar-dark bg-dark mb-4">
    <!-- Бренд и кнопка «Гамбургер» -->
    <a class="navbar-brand" href="{{ route('index') }}">Магазин</a>
    <button class="navbar-toggler" type="button" data-toggle="collapse"
            data-target="#navbar-example" aria-controls="navbar-larashop"
            aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <!-- Основная часть меню (может содержать ссылки, формы и прочее) -->
    <div class="collapse navbar-collapse" id="navbar-larashop">
        <!-- Этот блок расположен слева -->
        <ul class="navbar-nav mr-auto">
            <li class="nav-item">
                <a class="nav-link" href="{{ route('catalog.index') }}">Каталог</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="#">Доставка</a>
            </li>
            <li class="nav-item">
                <a class="nav-link" href="#">Контакты</a>
            </li>
        </ul>

        <!-- Этот блок расположен посередине -->
        <form class="form-inline my-2 my-lg-0">
            <input class="form-control mr-sm-2" type="search"
                   placeholder="Поиск по каталогу" aria-label="Search">
            <button class="btn btn-outline-info my-2 my-sm-0"
                    type="submit">Искать</button>
        </form>

        <!-- Этот блок расположен справа -->
        <ul class="navbar-nav ml-auto">
            <li class="nav-item">
                <a class="nav-link" href="{{ route('basket.index') }}">Корзина</a>
            </li>
        </ul>
    </div>
</nav>
<div class="col-md-6 mb-4">
    <div class="card list-item">
        <div class="card-header">
            <h3 class="mb-0">{{ $product->name }}</h3>
        </div>
        <div class="card-body p-0">
            <img src="https://via.placeholder.com/400x120" alt="" class="img-fluid">
        </div>
        <div class="card-footer">
            <!-- Форма для добавления товара в корзину -->
            <form action="{{ route('basket.add', ['id' => $product->id]) }}" 
                  method="post" class="d-inline">
                @csrf
                <button type="submit" class="btn btn-success">Добавить в корзину</button>
            </form>
            <a href="{{ route('catalog.product', ['slug' => $product->slug]) }}"
               class="btn btn-dark float-right">Перейти к товару</a>
        </div>
    </div>
</div>

Не слишком удачно, что получение объекта корзины реализовано в контроллере BasketController, это аукнулось позже, когда потребовалось получать этот объект не только в контроллере. Так что позже метод getBasket() пришлось перенести в модель Basket и сделать его статическим. Подробнее можно посмотреть здесь.

Поиск: 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.