• Время чтения ~8 мин
  • 23.03.2023
Image by Annie Ruygt

Fly берет образ Docker, преобразует его в виртуальную машину и запускает эту виртуальную машину в любой точке мира. Запустите приложение Laravel за считанные минуты!

Корзины покупок не опорожняются, когда пользователь покидает страницу. Почему? Ну, для этого мне нужно вернуть вас назад во времени. Покажите волнистый переход воспоминаний!

Когда-то давным-давно был обычный, обычный, нормальный парень по имени Джек. Джек любил делать покупки в Интернете и часто часами просматривал различные веб-сайты в поисках лучших предложений. Он добавлял товары в свою корзину, но, казалось, никогда не заказывал их. Вместо этого он часто проверял свою тележку, любуясь выбранными предметами, но никогда не нажимал кнопку «купить». Джек заказывал их только тогда, когда он был на сто процентов уверен, что хочет их.

Знаю ли я Джека?

Да, у меня есть привычка заполнять корзины покупок вещами, которые я закажу через несколько месяцев или просто забуду. Помимо сказок, созданных ChatGPT, о недостатках моего персонажа, мы здесь, чтобы поговорить о реальных героях истории: тележках для покупок, которые неблагодарно держат наши возможные будущие покупки в различных интернет-магазинах. Не забудьте поблагодарить их за их службу когда-нибудь.

Любой, давайте построим один, чтобы посмотреть, как они работают!

Это темы, которые затрагивает эта статья:

  • http сессия, и фасад сессии в Laravel
  • Livewire
  • Шаблон сервиса Денежный
  • пакет
  • Laravel коллекции Наша конечная цель будет заключаться в создании

The Goal

нового заказа со всеми продуктами, которые пользователь имеет в своей корзине покупок. Чтобы включить это, нам понадобятся некоторые компоненты Livewire для обновления значений без перезагрузки страницы, а также корзина покупок, которая отслеживает продукты наших пользователей (продукты пользователей?). Быстрый спойлер, мы будем использовать класс обслуживания для этого, а там мы будем использовать фасад Сессии Laravel.

Итак, мы будем строить очень простой интернет-магазин. Я постараюсь сделать его коротким и простым в два этапа: добавление продуктов в корзину и использование корзины для размещения заказа.

Брифинг окончен, давайте строить!

The Models

В нашем интернет-магазине barebones у нас есть 2 модели, о которых нужно беспокоиться:

  • таблица продуктов с идентификатором, временными метками, названием, описанием, ценой и скидкой. Он содержит несколько Ордеров.
  • таблица Заказов, принадлежащая одному Пользователю, имеющая идентификатор и временные метки и содержащая несколько Продуктов.

Поскольку это отношение «многие ко многим», нам понадобится сводная таблица. Он будет содержать иностранные идентификаторы для Продукта и Заказа, а также поля для скидки (скидка на продукт на момент заказа) и количество (заказанных продуктов). Вот как настроены модели Product и Order:

class Product extends Model
{
    //other stuff here

    public function orders()
    {
        return $this->belongsToMany(Order::class)->withPivot('discount', 'amount');
    }
}

class Order extends Model
{
    //other stuff here

    public function products()
    {
        return $this->belongsToMany(Product::class)->withPivot('discount', 'amount');
    }
}

Laravel автоматически примет имя сводной таблицы [alphabetically first model name]\_[alphabetically first model name], что order\_product в нашем случае. Ларавель также будет предполагать, что иностранные идентификаторы будут называться product_id и order_id. Этот withPivot метод позволяет нам добавлять дополнительные столбцы в таблицу, как discount и amount в нашем случае, как показано в документах.

Вот как будут выглядеть наши таблицы:

Adding Products to the Shopping Cart

Building the Shopping Cart Service

Служба корзины покупок будет обрабатывать все хранение и извлечение данных в / из сеанса http. Таким образом, мы можем повторно использовать код и внедрять его во все компоненты, где это необходимо.

Создайте папку app/Services и создайте там новый класс php с именем CartService.php. Прежде чем мы начнем с добавления продуктов в нашу корзину, нам нужно поговорить о том, какие данные мы собираемся сохранить. Я нашел наиболее производительный и безопасный способ:

  • Хранить только то, что нам нужно для отображения и / или создания order
  • Только сохранить скалярные типы (int, float, bool, string)

Итак, вот что мы собираемся сохранить в нашем CartService:

  • productId - для создания суммы order_product
  • - для отображения и для создания order_product
  • цены продукта - для отображения
  • названия продукта - для отображения
  • скидки - для отображения и для создания order_product

Чтобы сделать наши данные доступными для поиска, мы также дадим им productId в качестве ключа. Вот как это будет выглядеть:

749 => [
  'productId' => 749,
  'amount' => 5,
  'price' => 1499, //always save price as an int!
  'name' => 'Creative product name here',
  'discount' => 0,
]

Хорошо, у нас есть все, что нам нужно сейчас. Создайте функцию addToCart в CartService:

public function addToCart(int $productId): array
{
    // get data from session (this equals Session::get(), use empty array as default)
    $shoppingCart = session('shoppingCart', []);

    if (isset($shoppingCart[$productId]))
    {
        // product is already in shopping cart, increment the amount
        $shoppingCart[$productId]['amount'] += 1;
    }
    else
    {
        // fetch the product and add 1 to the shopping cart
        $product = Product::findOrFail($productId);
        $shoppingCart[$productId] = [
            'productId' => $productId,
            'amount'    => 1,
            'price'     => $product->price->getAmount(),
            'name'      => $product->name,
            'discount'  => $product->discount
        ];
    }

    // update the session data (this equals Session::put() )
    session(['shoppingCart' => $shoppingCart]);
    return $shoppingCart;
}

Поскольку мы используем идентификатор продукта в качестве ключа, мы можем легко проверить, содержит ли корзина уже продукт с помощью isset($shoppingCart[$productId]) . Если корзина уже содержит продукт, мы можем легко увеличить сумму, в противном случае нам нужно получить продукт и сохранить все его свойства в новом элементе массива.

Удаление товаров из корзины довольно похоже: проверьте, есть ли он в корзине (он должен!) и проверьте сумму. Если это 1, полностью удалите элемент из массива, а в противном случае просто уменьшите сумму. Вот как это выглядит:

public function removeFromCart(int $productId): array | null
{
    $shoppingCart = session('shoppingCart', []);

    if (!isset($shoppingCart[$productId]))
    {
        // should not happen, and should throw an error.
        return null;
    }
    else
    {
        if ($shoppingCart[$productId]['amount'] == 1){
            unset($shoppingCart[$productId]);
        }
        else
        {
            $shoppingCart[$productId]['amount'] -= 1;
        }
    }

    session(['shoppingCart' => $shoppingCart]);
    return $shoppingCart;
}

Теперь давайте использовать наши совершенно новые методы, не так ли?

Во-первых, чтобы компоненты Livewire могли использовать CartService, им понадобится productId. В моем примере я сделал компонент Livewire для кнопки «добавить в корзину» и дал ему productId, пока я зацикливался на продуктах для отображения. Это выглядело примерно так:

@foreach($products as $product)
    <tr>
        <td>{{$product->name}}</td>
        <td>
            <livewire:add-to-cart-button :productId="$product->id"></livewire:add-to-cart-button>
        </td>
    </tr>
@endforeach

Второе, что понадобится компонентам, это сам CartService, где пригодится внедрение зависимостей Laravel: просто передайте CartService в качестве параметра в методе компонента Livewire, и Laravel позаботится о бизнесе. Вот как выглядит мой addToCart метод:

public function addToCart(CartService $cartService)
{
    $cartService->addToCart($this->productId);
}

CartService внедрен и готов к использованию, где бы он ни был добавлен. Легко-пиздно лимонно-сдавливающее!

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

В mount хуке жизненного цикла Livewire я позволю компоненту получить содержимое корзины покупок:

public function mount(CartService $cartService)
{
    $this->shoppingCart = $cartService->getShoppingCart();
}

И при изменении сумм я буду использовать возвращаемое значение методов CartService:

public function incrementAmount(int $productId, CartService $cartService)
{
    $this->shoppingCart = $cartService->addToCart($productId);
}

Здесь есть проблема: Что, если на экране есть два компонента, которые необходимо обновить? Например, обзор корзины покупок и кнопка корзины покупок в навигационной панели?

Refreshing Livewire Views

Не волнуйтесь, Livewire поможет вам! Просто вызывайте событие (я бы предложил «updateShoppingCart» или что-то подобное) и слушайте его на компонентах, которые будут нуждаться в обновлении. Вот так:

// in the 'incrementAmount' method
$this->emit('updateShoppingCart');

// in the shoppingCartButton component:
class ShoppingCartButton extends Component
{
    //public properties here...
    protected $listeners = ['updateShoppingCart' => 'updateShoppingCart'];

  public function updateShoppingCart(CartService $cartService)
    {
        $this->shoppingCart = $cartService->getShoppingCart();
        $this->cartAmount = $cartService->getCartAmount();
        $this->subTotal = $cartService->getCartSubTotal()->format();
    }

Я быстро коснусь cartAmount и subTotal здесь: getCartAmount()метод просто подсчитывает все суммы всех элементов массива в корзине покупок. Это в основном то, сколько штук в корзине: 2 яблока и 3 банана составляют 5 в общей сложности.

Это subTotal сумма за каждый price x amount продукт, поэтому 2 яблока по 1 доллару за каждый и 3 банана за 10 долларов получат subTotal 32 доллара. Чтобы упростить форматирование, я использую пакет laravel-money. getCartSubtotal вернет Money объект, который может быть отформатирован так, как вам нравится с помощью методаformat().

Бум, заполнение корзины сделано и запылено! Теперь давайте наденем наши бэкэнд-шляпы и проверим процесс оформления заказа. Понимаете?

Размещайте свои серверы близко к своим пользователям и удивляйтесь скорости непосредственной близости. Глобальное развертывание на Fly за считанные минуты!

Разверните приложение Laravel!  →

Creating Orders With a Many-to-many Relationship

Хорошо, у наших клиентов теперь есть цифровой помощник, удерживающий все выбранные ими продукты, пока они продолжают просмотр. Понимаете?

Далее: фактическое создание Заказа из корзины товаров. Во-первых, отправьте содержимое корзины покупок в качестве входных данных формы:

<form method="post" action="{{route('orders.store')}}">
    @csrf
    <input type="hidden" name="orderProducts" id="orderProducts" value="{{json_encode($shoppingCart)}}">
    <button type="submit">{{__('Order')}}</button>
</form>

просто json_encode массив и отправьте его в виде строки. Поправьте свою заднюю шляпу и присоединитесь ко мне, StoreOrderRequest чтобы посмотреть на проверку.

Validating a FormRequest With an Array

Вы можете просто использовать правило проверкиjson, но я настоятельно рекомендую вам декодировать его в FormRequest и проверить его как массив. Это более гибкий способ: Правила проверки:

protected function prepareForValidation()
{
    /**
     * We need to decode the JSON, so we can validate it as an array.
     * This is how the original input data looks:
     * $productId => ['amount' => $amount, 'price' => $price, 'name' => $name, 'discount' => $discount, 'productId' => $productId]
     */
    $this->merge(['orderProducts' => json_decode($this->orderProducts, true)]);
}

return [
    'orderProducts.*.productId' => ['required', 'integer'],
    'orderProducts.*.amount' => ['required', 'integer', 'min:1'],
    'orderProducts.*.discount' => ['required', 'numeric', 'between:0,100'],
];

ОзначаетorderProducts.*., что правило проверки должно применяться к каждому элементу массиваorderProducts. Таким образом, мы можем проверить вплоть до сингулярных свойств.

Хорошо, финишная прямая: сохранение Заказа в базе данных!

Attaching Intermediate Tables to a Model

Да, мы получили данные, и все выглядит хорошо: были проведены проверки анкетных данных, и металлоискатели не сигналили нам. Давайте создадим наш Заказ прямо сейчас!

Как всегда, Laravel делает это довольно легко для нас: мы можем использовать attach() метод на связи, чтобы добавить строку в промежуточную таблицу, напримерbelongsToMany: Это

$order->products()->attach($productId);

добавит промежуточную таблицу между $order Product и ID $productId. Добавить скидку и сумму к ней также не сложно: просто добавьте пары key=> значений после $productId:

$order->products()->attach($productId, ['amount' => $amount], 'discount' => $discount]]);

Now, нам просто нужно сделать это для каждого order_product мы получаем из корзины покупок. Это массив, помните? Вот как выглядит окончательная версия:

$validated = $request->validated();

$order = new Order();

foreach ($validated['orderProducts'] as $orderProduct)
{
    $order->products()->attach($orderProduct['productId'], ['amount' => $orderProduct['amount'], 'discount' => $orderProduct['discount']]);
}
$order->save();

теперь пользователь может заказать просто отлично, но есть одна деталь, которую нужно исправить: корзина покупок не опустошается после заказа. Я позволю вам разобраться в этом самостоятельно, но я уверен, что session→forget() метод пригодится.

Как всегда, спасибо за прочтение!

Comments

No comments yet
Yurij Finiv

Yurij Finiv

Full stack

Про мене

Professional Fullstack Developer with extensive experience in website and desktop application development. Proficient in a wide range of tools and technologies, including Bootstrap, Tailwind, HTML5, CSS3, PUG, JavaScript, Alpine.js, jQuery, PHP, MODX, and Node.js. Skilled in website development using Symfony, MODX, and Laravel. Experience: Contributed to the development and translation of MODX3 i...

Об авторе CrazyBoy49z
WORK EXPERIENCE
Контакты
Ukraine, Lutsk
+380979856297