Laravel по-русски

Русское сообщество разработки на PHP-фреймворке Laravel.

Ты не вошёл. Вход тут.

#1 19.03.2017 21:19:36

работа с сессиями для реализации корзины

Добрый вечер!
Я новичок в Laravel. Моего знания хватило для создания простого lending page, с картинками портфолио, и админки для размещения их на сайте.
Возникла задача, а как к ней подступиться не знаю. Создать корзину. Принцип действия корзины прост, кликнуть на картинку (витрина на сайте), картинка помещается в корзину (т.е. формируется заказ), и отправляется файл (например excel) владельцу сайта.
Я понимаю, что это работа с сессиями. То есть информация которая должна находится в корзине - записывается в сессию и хранится там до того момента как нужно корзину очистить. При этом что хранить в сессии - это уже зависит от функционала, как правило это идентификатор необходимого товара. Вот только как и с чего начать в Laravel не знаю. Может у кого то есть пример похожего кода?

Не в сети

#2 20.03.2017 05:57:52

Re: работа с сессиями для реализации корзины

ну вот тебе с одного из моих проектов

app/Services/Cart.php:

<?php namespace App\Services;

use App\CartItem;
use App\Product;
use App\Exceptions\CartException as Exception;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Http\Request;
use Illuminate\Session\Store as Session;

class Cart {

	/**
	 * @var Session
	 */
	private $session;

	/**
	 * @var Guard
	 */
	private $auth;

	/**
	 * @var CartItem
	 */
	private $model;

	/**
	 * @var string
	 */
	private $code;


	function __construct(Request $request, Guard $auth, CartItem $model)
	{
		$this->session = $request->session();
		$this->auth = $auth;
		$this->model = $model;

		$this->code = $this->session->get('cart.code');
		if (is_null($this->code)) {
			$this->createNewCart();
		}
	}


	public function code()
	{
		return $this->code;
	}


	public function clear()
	{
		$this->session->remove('cart');
		$this->model->whereCode($this->code)->delete();
		$this->createNewCart();
	}


	/**
	 * Сумма стоимостей всех позиций в корзине. Итоговая сумма на чеке.
	 *
	 * @return float
	 */
	public function total()
	{
		$total = $this->session->get('cart.total');

		if (is_null($total)) {
			/** @var Collection $items */
			$items = $this->model->with([ 'product' => function ($query) { $query->select([ 'id', 'price' ]); } ])
								 ->whereCode($this->code)
								 ->get([ 'product_id', 'quantity' ]);

			$total = $items->sum(function ($item) { return $item->quantity * $item->product->price; });

			$this->session->set('cart.total', $total);
		}

		return $total;
	}


	/**
	 * Суммарное количество единиц товара в заказе.
	 *
	 * @return int
	 */
	public function count()
	{
		$count = $this->session->get('cart.count');

		if (is_null($count)) {

			$count = $this->model->whereCode($this->code)->sum('quantity');

			$this->session->set('cart.count', $count);
		}

		return $count;
	}


	/**
	 * Позиции заказа.
	 *
	 * @param array $columns
	 * @param bool  $lock
	 *
	 * @return Collection
	 */
	public function items($columns = [ '*' ], $lock = false)
	{
		if ($lock) {
			return $this->model->with(
				[
					'product' => function ($query) {
						$query->lockForUpdate();
					}
				]
			)->whereCode($this->code)->latest('created_at')->lockForUpdate()->get($columns);
		}

		return $this->model->with('product')->whereCode($this->code)->latest('created_at')->get($columns);
	}


	/**
	 * Изменения количества позиции в заказе. Проверяет наличие на складе. Не проверяет баланс пользователя.
	 *
	 * @param string|array $condition
	 * @param int          $quantity
	 *
	 * @throws Exception
	 */
	public function setQuantity($condition, $quantity)
	{
		if (!is_array($condition)) {
			$condition = [ 'product_id' => $condition ];
		}

		// проверка наличия на складе

		$item = $this->model->with('product')->whereCode($this->code)->where($condition)->first();

		if ($quantity > $item->product->quantity) {
			throw new Exception(setting('message.not_enough_in_stock') ?: 'Количество на складе ограничено.');
		}

		$item->update(compact('quantity'));

		$this->clearCounters();
	}


	/**
	 * @param Product $product
	 * @param int     $quantity
	 *
	 * @return CartItem
	 * @throws Exception
	 */
	public function addItem($product, $quantity = 1)
	{
		if ($this->auth->guest()) {
			abort(401);
		}

		if (!$this->auth->user()->is_admin) {

			// перед добавлением товара в корзину надо проверить баланс пользователя
			// если баллов не хватает, контроллеру возвращается false для того
			// чтобы тот мог уведомить пользователя надлежащим способом

			$possible_total = $this->total() + $product->price;

			if ($possible_total > $this->auth->user()->balance) {

				throw new Exception(
					setting(
						'message.not_enough_points'
					) ?: 'На вашем балансе недостаточно баллов для выполнения этого действия.'
				);

			}
		}

		// если товар уже есть в корзине - только увеличим его количество

		/** @var CartItem $item */
		$item = $this->model->whereCode($this->code)->whereProductId($product->getKey())->first();

		if ($item) {

			// проверка наличия на складе

			if ($item->quantity + $quantity > $product->quantity) {
				throw new Exception(setting('message.not_enough_in_stock') ?: 'Количество на складе ограничено.');
			}

			$item->increment('quantity', $quantity);

			$this->clearCounters();

			return $item;
		}

		// проверка наличия на складе

		if ($quantity > $product->quantity) {
			throw new Exception(setting('message.not_enough_in_stock') ?: 'Количество на складе ограничено.');
		}

		// добавление нового товара в корзину

		/** @var CartItem $newItem */
		$newItem = $this->model->create(
			[
				'code' => $this->code,
				'user_id' => $this->auth->user()->getAuthIdentifier(),
				'product_id' => $product->getKey(),
				'quantity' => $quantity,
			]
		);

		$this->clearCounters();

		return $newItem;
	}


	public function deleteItem($condition)
	{
		if (!is_array($condition)) {
			$condition = [ 'product_id' => $condition ];
		}

		$this->model->whereCode($this->code)->where($condition)->delete();

		$this->clearCounters();
	}


	private function createNewCart()
	{
		$this->code = str_random();
		$this->session->set('cart.code', $this->code);
		$this->clearCounters();
	}


	private function clearCounters()
	{
		$this->session->remove('cart.count');
		$this->session->remove('cart.total');
	}
}

app/CartItem.php:

<?php namespace App;

use Illuminate\Database\Eloquent\Model;

/**
 * App\CartItem
 *
 * @property integer $id 
 * @property string $code 
 * @property integer $user_id 
 * @property integer $product_id 
 * @property integer $quantity 
 * @property \Carbon\Carbon $created_at 
 * @property \Carbon\Carbon $updated_at 
 * @property-read \App\User $user 
 * @property-read \App\Product $product 
 * @method static \Illuminate\Database\Query\Builder|\App\CartItem whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\CartItem whereCode($value)
 * @method static \Illuminate\Database\Query\Builder|\App\CartItem whereUserId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\CartItem whereProductId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\CartItem whereQuantity($value)
 * @method static \Illuminate\Database\Query\Builder|\App\CartItem whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\App\CartItem whereUpdatedAt($value)
 */
class CartItem extends Model {

	/**
	 * The attributes that should be casted to native types.
	 *
	 * @var array
	 */
	protected $casts = [ 'quantity' => 'integer' ];

	/**
	 * The attributes that are mass assignable.
	 *
	 * @var array
	 */
	protected $fillable = [ 'code', 'user_id', 'product_id', 'quantity' ];


	public function user()
	{
		return $this->belongsTo('App\User');
	}


	public function product()
	{
		return $this->belongsTo('App\Product');
	}

}

app/Product.php:

<?php namespace App;

use App\Events\CatalogWasChanged;
use Illuminate\Database\Eloquent\Model;

/**
 * App\Product
 *
 * @property integer $id
 * @property string $name
 * @property string $photo
 * @property string $sticker
 * @property string $description
 * @property string $properties
 * @property float $price
 * @property float $old_price
 * @property integer $quantity
 * @property boolean $published
 * @property boolean $archived
 * @property integer $top_position
 * @property integer $updated_by
 * @property string $updated_ip
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\ProductCategory[] $categories
 * @property-read \App\User $editor
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\User[] $likes
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereName($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product wherePhoto($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereSticker($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereDescription($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereProperties($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product wherePrice($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereOldPrice($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereQuantity($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product wherePublished($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereArchived($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereTopPosition($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereUpdatedBy($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereUpdatedIp($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Product whereUpdatedAt($value)
 * @method static \App\Product available()
 */
class Product extends Model {

	/**
	 * The attributes that should be casted to native types.
	 *
	 * @var array
	 */
	protected $casts = [
		'published' => 'boolean',
		'archived' => 'boolean',
		'properties' => 'object',
        'sort_order' => 'integer',
	];

	/**
	 * The attributes that are mass assignable.
	 *
	 * @var array
	 */
	protected $fillable = [
		'name',
		'photo',
		'sticker',
		'description',
		'properties',
		'price',
		'old_price',
		'quantity',
		'published',
		'archived',
		'sort_order',
		'updated_by',
		'updated_ip'
	];

	/**
	 * The attributes that should be hidden for arrays.
	 *
	 * @var array
	 */
	protected $hidden = [ 'updated_ip' ];


	public static function boot()
	{
		parent::boot();
		static::created(function () { event(new CatalogWasChanged); });
		static::saved(function () { event(new CatalogWasChanged); });
	}


	public function categories()
	{
		return $this->belongsToMany('App\ProductCategory');
	}


	public function editor()
	{
		return $this->belongsTo('App\User', 'updated_by');
	}


	public function likes()
	{
		return $this->belongsToMany('App\User', 'product_user_likes');
	}


	public function scopeAvailable($query)
	{
		return $query->wherePublished(true)->whereArchived(false)->where('quantity', '>', 0);
	}
}

сам разберёшься? миграции для cart_items и products сам напишешь?

Не в сети

#3 20.03.2017 08:41:31

Re: работа с сессиями для реализации корзины

constb пишет:

сам разберёшься? миграции для cart_items и products сам напишешь?

Спасибо огромное!
Мысль уловил, направление понял.
Буду пробовать сам, иначе не научусь.
Если уж совсем непонятно будет, спрошу совета.
Еще раз спасибо!

Не в сети

#4 20.03.2017 16:20:25

Re: работа с сессиями для реализации корзины

constb пишет:

сам разберёшься? миграции для cart_items и products сам напишешь?

Подскажите, создал файл миграции 2017_03_20_120058_create_table_products.php

    public function up()
    {
        Schema::create('products', function (Blueprint $table) {
            $table->increments('id');
 			$table->string ('name',100);
			$table->string ('photo',100);
			$table->string ('sticker',100);
			$table->string ('description',100);
			$table->string ('properties',100);
			$table->string ('price',100);
			$table->string ('old_price',100);
			$table->string ('quantity',100);
			$table->string ('published',100);
			$table->string ('archived',100);
			$table->string ('sort_order',100);
			$table->string ('updated_by',100);
			$table->string ('updated_ip',100);
                        $table->timestamps();
        });
    }

А вот что создавать во второй миграции 2017_03_20_115759_create_table_cart_items.php, я не совсем понимаю, что-то такое?

    public function up()
    {
        Schema::create('cart_items', function (Blueprint $table) {
            $table->increments('id');
            $table->string ('name',100);
	        $table->integer('id',100);
 			$table->string ('code',100);
 			$table->integer ('user_id',100);
 			$table->integer ('product_id',100);
 			$table->integer ('quantity',100);
                        $table->timestamps();
        });
    }

Я по правильному пути иду?

Не в сети

#5 20.03.2017 19:27:10

Re: работа с сессиями для реализации корзины

ну примерно. у меня свойства товаров определяются ТЗ, там есть вещи, которые тебе наверное не понадобятся. например я сохраняю кто из менеджеров последним менял свойства товара и с какого айпишника – достаточно специфичная задача. то же самое с архивом товаров, «зачёркнутой» ценой и сортировкой с админки вручную. начни с простого: name, quantity, price, а остальное добавляй по необходимости. в корзине ключом является рандомный код, которых сохранён в сессии, но при этом я запоминаю владельца корзины – это тоже кстати опциональный функционал – мне он нужен был для страницы с «брошенными корзинами», которые потом обрабатывают менеджеры и «добивают» клиентов. у меня миграция выглядит так:

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateCartItemsTable extends Migration {

	/**
	 * Run the migrations.
	 *
	 * @return void
	 */
	public function up()
	{
		Schema::create('cart_items', function(Blueprint $table)
		{
			$table->engine = 'InnoDB';

			$table->increments('id');

			$table->string('code', 16)->index();
			$table->integer('user_id')->unsigned();
			$table->integer('product_id')->unsigned();
			$table->integer('quantity')->unsigned();

			$table->timestamps();
		});
		Schema::table('cart_items', function(Blueprint $table)
		{
			$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
			$table->foreign('product_id')->references('id')->on('products')->onDelete('cascade');
		});
	}

	/**
	 * Reverse the migrations.
	 *
	 * @return void
	 */
	public function down()
	{
		Schema::drop('cart_items');
	}

}

внешние ключи удаляют записи из таблицы при удалении товаров или пользователей, тоже кстати на всякий случай добавлено – на самом деле у меня ни товары ни пользователи никогда полностью не удаляются, товары уходят в архив, а пользователи удаляются мягко, через трест SoftDeletes. кроме того, у меня достаточно старая версия ларавеля в этом проекте (5.0), в ней нет ->unsignedInteger(), приходится писать явно

Не в сети

#6 20.03.2017 19:54:12

Re: работа с сессиями для реализации корзины

Мне действительно много из этого не нужно. Только самый простой вариант: name, quantity, price. Использую версию Laravel 5.2. Спасибо за помощь!

Не в сети

#7 21.03.2017 01:32:01

Re: работа с сессиями для реализации корзины

Abs пишет:

Использую версию Laravel 5.2. Спасибо за помощь!

Полез в файл routes.php  и не нашел его, и вспомнил, что обновлялся до 5.4.6.

Возник вопрос, вот я попытался прописать маршруты в файле web, для: Cart.php, Product.php, CartItem.php и задумался.
Раньше я прописывал группу маршрутов контроллерам, которые делают ту или иную операцию, и вынесены они отдельно в файлы.
Например (упрощенно) файл web для Portfolio:

	Route::group(['prefix'=>'portfolios'],function() {
			
		Route::get('/',['uses'=>'PortfoliosController@execute','as'=>'portfolio']);
				
		Route::match(['get','post'],'/add',['uses'=>'PortfoliosAddController@execute','as'=>'portfoliosAdd']);
		
		Route::match(['get','post','delete'],'/edit/{portfolio}',['uses'=>'PortfoliosEditController@execute','as'=>'portfoliosEdit']);
		
	});

Соответственно в IndexController.php контролере:

 $portfolios = Portfolio::get(array('name','filter','images','price'));

В Portfolio.php:

class Portfolio extends Model
{
  	protected $fillable = ['name','filter','images','price'];
}

И для админки PortfoliosController.php, PortfoliosAddController.php, PortfoliosEditController.php с функциями добавления,редактирования,удаления.

А значит по аналогии, мне нужно зарегистрировать в web: Cart.php, Product.php, CartItem.php. Затем закомментировать не используемые мною функции, типа "проверка наличия на складе" в Cart.php. Создать кнопку корзина. Создать условие для клика мышкой на картинку, для помещения этой позиции в корзину, В корзине выставить нужное кол-во, и ... А тут моя фантазия закончилась. По идее, содержимое корзины нужно вывести в файл(.xls), и отправить заказ владельцу сайта.

Я вообще правильно рассуждаю? А то может вообще пошел огородами...

Не в сети

#8 21.03.2017 06:04:03

Re: работа с сессиями для реализации корзины

зачем в маршрутах регистрировать модели? что-то и правда огородами )

Не в сети

#9 21.03.2017 10:37:28

Re: работа с сессиями для реализации корзины

constb пишет:

зачем в маршрутах регистрировать модели? что-то и правда огородами )

Действительно зачем? Я невнимателен. Не обратил внимания на подключение Eloquent. Спасибо!

Не в сети

#10 24.03.2017 13:55:06

Re: работа с сессиями для реализации корзины

constb пишет:

зачем в маршрутах регистрировать модели? что-то и правда огородами )

Добрый день!

Я не правильно написал, регистрировать в маршрутах нужно шаблон корзины. Мне не понятен был сам механизм (цепочка) работы корзины.
Размышляя, что такое корзина - это страница (модальное окошко) на которой выводится информация из сессии. Т.е. пользователь выбрав товар - нажав на картинку (ссылку) кладет его в корзину -сессия записывается в базу (отрабатывает модель). Открывая же страничку корзины, из базы в нее вытаскиваются данные (массив данных из модели). Но Вы прислали три модели. И я не совсем понимаю назначение третьей. Cart.php, Product.php, CartItem.php - уточните пожалуйста назначение третьей модели.

Не в сети

#11 24.03.2017 19:45:00

Re: работа с сессиями для реализации корзины

Cart – это не модель, это сервис уровня приложения, который обеспечивает апи для работы с корзиной в коде сайта. он существует как раз для того чтобы с CartItem-ами не приходилось работать «руками». в сессии находится только идентификатор корзины, её содержимое как раз определяется записями в таблице cart_items, а модель CartItem – её представление в элоквент.

учитывая задаваемые вопросы, могу посоветовать либо вдумчиво почитать документацию, либо найти специалиста, который напишет сайт за деньги. меня находить не надо – я и так на двух работах работаю без выходных уже smile

Не в сети

#12 24.03.2017 20:36:06

Re: работа с сессиями для реализации корзины

constb пишет:

Cart – это не модель, это сервис уровня приложения, который обеспечивает апи для работы с корзиной в коде сайта. он существует как раз для того чтобы с CartItem-ами не приходилось работать «руками». в сессии находится только идентификатор корзины, её содержимое как раз определяется записями в таблице cart_items, а модель CartItem – её представление в элоквент.

учитывая задаваемые вопросы, могу посоветовать либо вдумчиво почитать документацию, либо найти специалиста, который напишет сайт за деньги. меня находить не надо – я и так на двух работах работаю без выходных уже smile

Спасибо за ответ. Извиняюсь за беспокойство. Я новичок в Laravel, о чем писал в первом посте. Учусь, поставил себе задачу, и пытаюсь грызть гранит науки. Как оказалось, это не такая простая задача. Еще раз спасибо, и желаю успеха в работе.

Не в сети

#13 07.04.2017 16:47:45

Alexsaab
Откуда: Москва
Сообщений: 92

Re: работа с сессиями для реализации корзины

Не стал заморачиваться с корзиной и установил пакет для Laravel! Что ставить?! Поищите в поиске: shopping cart laravel. В итоге имеем много различных корзин - например, одну для откладывания товара (для сравнения) вторую для шопинга (именно корзину).

Не в сети

#14 08.04.2017 02:27:40

Re: работа с сессиями для реализации корзины

Спасибо за совет! Сам я, за это время, просмотрел несколько видео уроков, и в каждом делали корзину на loravel. Но мой сайт не состоит из одной страницы (как в уроках), в нем каркас, и виды. А значит не совпадают маршруты к контролерам и т.д. Я не понимаю, как работают разные методы, например execut, и сижу, вот изучаю маршрутизацию. До этого у меня не было понимания, что представляет из себя корзина. Оказывается есть множество способов реализации на Laravel, такие как сессии, куки, сохранение в БД. Или реализация например на JS, с отправкой на AJAX. Но главное я понял, какие шаги нужно сделать последовательно для её создания.

Не в сети

Подвал раздела