mivita/packages/digital-bird/shoppingcart/src/Cart.php
Kevin Adametz bfa3bb1df4 08 2024
2024-08-05 12:05:24 +02:00

842 lines
21 KiB
PHP

<?php
namespace Gloudemans\Shoppingcart;
use Closure;
use DateTime;
use Gloudemans\Shoppingcart\Traits\CartHelper;
use Illuminate\Support\Collection;
use Illuminate\Session\SessionManager;
use Illuminate\Database\DatabaseManager;
use Illuminate\Contracts\Events\Dispatcher;
use Gloudemans\Shoppingcart\Contracts\Buyable;
use Gloudemans\Shoppingcart\Exceptions\UnknownModelException;
use Gloudemans\Shoppingcart\Exceptions\InvalidRowIDException;
use Gloudemans\Shoppingcart\Exceptions\CartAlreadyStoredException;
class Cart
{
use CartHelper;
const DEFAULT_INSTANCE = 'default';
public $items;
public $fees;
/**
* Instance of the session manager.
*
* @var \Illuminate\Session\SessionManager
*/
protected $session;
/**
* Instance of the event dispatcher.
*
* @var \Illuminate\Contracts\Events\Dispatcher
*/
private $events;
/**
* Holds the current cart instance.
*
* @var string
*/
private $instance;
/**
* Cart constructor.
*
* @param \Illuminate\Session\SessionManager $session
* @param \Illuminate\Contracts\Events\Dispatcher $events
*/
public function __construct(SessionManager $session, Dispatcher $events)
{
$this->items = new Collection;
$this->fees = new Collection;
$this->session = $session;
$this->events = $events;
$this->instance(self::DEFAULT_INSTANCE);
}
/**
* Set the current cart instance.
*
* @param string|null $instance
* @return \Gloudemans\Shoppingcart\Cart
*/
public function instance($instance = null)
{
$instance = $instance ?: self::DEFAULT_INSTANCE;
$this->instance = sprintf('%s.%s', 'cart', $instance);
return $this;
}
/**
* Get the current cart instance.
*
* @return string
*/
public function currentInstance()
{
return str_replace('cart.', '', $this->instance);
}
/**
* Add an item to the cart.
*
* @param $id
* @param $name
* @param $qty
* @param $price
* @param $taxRate
* @param $taxIncluded
* @param array $options
* @param array $eventOptions
* @return array|array[]|CartItem|CartItem[]
*/
public function add($id, $name = null, $qty = null, $price = null, $taxRate = null, $taxIncluded = false, array $options = [], array $eventOptions = [])
{
if ($this->isMulti($id)) {
return array_map(function ($item) {
return $this->add($item);
}, $id);
}
if ($id instanceof CartItem) {
$cartItem = $id;
} else {
$cartItem = $this->createCartItem($id, $name, $qty, $price, $taxRate, $taxIncluded, $options);
}
$content = $this->getContent();
if ($content->has($cartItem->rowId)) {
$cartItem->qty += $content->get($cartItem->rowId)->qty;
}
$content->put($cartItem->rowId, $cartItem);
$this->items = $content;
$this->session->put($this->instance, $this->toArray());
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
'cartItem' => $cartItem,
], $eventOptions);
$this->events->dispatch('cart.added', [
$eventOptions,
]);
return $cartItem;
}
/**
* Update the cart item with the given rowId.
*
* @param $rowId
* @param $qty
* @param array $eventOptions
* @return CartItem|void
*/
public function update($rowId, $qty, array $eventOptions = [])
{
$cartItem = $this->get($rowId);
if ($qty instanceof Buyable) {
$cartItem->updateFromBuyable($qty);
} elseif (is_array($qty)) {
$cartItem->updateFromArray($qty);
} else {
$cartItem->qty = $qty;
}
$content = $this->getContent();
if ($rowId !== $cartItem->rowId) {
if ($content->has($cartItem->rowId)) {
$existingCartItem = $this->get($cartItem->rowId);
$cartItem->setQuantity($existingCartItem->qty + $cartItem->qty);
}
$content = $content->mapWithKeys(function ($val, $key) use ($rowId, $cartItem) {
if ($key === $rowId) {
return [ $cartItem->rowId => $cartItem ];
}
return [ $key => $val ];
});
$this->items = $content;
}
if ($cartItem->qty <= 0) {
$this->remove($cartItem->rowId);
return;
}
$this->session->put($this->instance, $this->toArray());
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
'cartItem' => $cartItem,
], $eventOptions);
$this->events->dispatch('cart.updated', [
$eventOptions
]);
return $cartItem;
}
/**
* Remove the cart item with the given rowId from the cart.
*
* @param $rowId
* @param array $eventOptions
* @return void
*/
public function remove($rowId, array $eventOptions = [])
{
$cartItem = $this->get($rowId);
$content = $this->getContent();
$content->pull($cartItem->rowId);
$this->items = $content;
$this->session->put($this->instance, $this->toArray());
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
'cartItem' => $cartItem,
], $eventOptions);
$this->events->dispatch('cart.removed', [
$eventOptions
]);
}
/**
* Get a cart item from the cart by its rowId.
*
* @param string $rowId
* @return \Gloudemans\Shoppingcart\CartItem
*/
public function get($rowId)
{
$content = $this->getContent();
if ($content->has($rowId) === false) {
throw new InvalidRowIDException("The cart does not contain rowId {$rowId}.");
}
return $content->get($rowId);
}
/**
* Destroy the current cart instance.
*
* @return void
*/
public function destroy()
{
$this->session->remove($this->instance);
}
/**
* Get the content of the cart.
*
* @return \Illuminate\Support\Collection
*/
public function content()
{
return $this->getContent();
}
/**
* Get the number of items in the cart.
*
* @return int|float
*/
public function count()
{
$content = $this->getContent();
return $content->sum('qty');
}
/**
* Get the total price of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return string
*/
public function total($decimals = null, $decimalPoint = null, $thousandSeperator = null, $withFees = true)
{
$content = $this->getContent();
$total = $content->reduce(function ($total, CartItem $cartItem) {
return $total + ($cartItem->total);
}, 0);
if ($withFees === true) {
$fees = $this->feeTotal(null, null, null, true);
$total = $total + $fees;
}
$decimals = is_null($decimals) ? config('cart.format.total_decimals') : $decimals;
return $this->numberFormat($total, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Get the total tax of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return float
*/
public function tax($decimals = null, $decimalPoint = null, $thousandSeperator = null, $withFees = true)
{
$content = $this->getContent();
$tax = $content->reduce(function ($tax, CartItem $cartItem) {
return $tax + ($cartItem->taxTotal);
}, 0);
if ($withFees === true) {
$fees = $this->feeTax();
$tax = $tax + floatval($fees);
}
$decimals = is_null($decimals) ? config('cart.format.tax_decimals') : $decimals;
return $this->numberFormat($tax, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Get the total tax of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return float
*/
public function feeTax($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$content = $this->getContent();
$tax = 0;
foreach ($this->getFees() as $fee) {
$tax += $fee->tax;
}
$decimals = is_null($decimals) ? config('cart.format.fee_total_tax_decimals') : $decimals;
return $this->numberFormat($tax, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Get the subtotal (ex tax) of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return float
*/
public function subtotal($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$content = $this->getContent();
$subTotal = $content->reduce(function ($subTotal, CartItem $cartItem) {
return $subTotal + ($cartItem->subtotal);
}, 0);
$decimals = is_null($decimals) ? config('cart.format.subtotal_ex_tax_decimals') : $decimals;
return $this->numberFormat($subTotal, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Get the subtotal (total - tax) of the items in the cart.
*
* @param int $decimals
* @param string $decimalPoint
* @param string $thousandSeperator
* @return float
*/
public function subtotalTax($decimals = null, $decimalPoint = null, $thousandSeperator = null)
{
$content = $this->getContent();
$subTotal = $content->reduce(function ($subTotal, CartItem $cartItem) {
return $subTotal + ($cartItem->subtotalTax);
}, 0);
$decimals = is_null($decimals) ? config('cart.format.subtotal_inc_tax_decimals') : $decimals;
return $this->numberFormat($subTotal, $decimals, $decimalPoint, $thousandSeperator);
}
/**
* Search the cart content for a cart item matching the given search closure.
*
* @param Closure $search
* @return \Illuminate\Support\Collection
*/
public function search(Closure $search)
{
$content = $this->getContent();
return $content->filter($search);
}
/**
* Associate the cart item with the given rowId with the given model.
*
* @param string $rowId
* @param mixed $model
* @return void
*/
public function associate($rowId, $model)
{
if (
is_string($model) === true &&
class_exists($model) === false
) {
throw new UnknownModelException("The supplied model {$model} does not exist.");
}
$cartItem = $this->get($rowId);
$cartItem->associate($model);
$content = $this->getContent();
$content->put($cartItem->rowId, $cartItem);
$this->session->put($this->instance, $this->toArray());
}
/**
* Set the tax rate for the cart item with the given rowId.
*
* @param string $rowId
* @param int|float $taxRate
* @return void
*/
public function setTax($rowId, $taxRate)
{
$cartItem = $this->get($rowId);
$cartItem->setTaxRate($taxRate);
$content = $this->getContent();
$content->put($cartItem->rowId, $cartItem);
$this->items = $content;
$this->session->put($this->instance, $this->toArray());
}
/**
* Store an the current instance of the cart.
*
* @param $identifier
* @param array $eventOptions
* @return void
*/
public function store($identifier, array $eventOptions = [])
{
// Remove any existing identifiers
// Although possibly first or update could work in future
$this
->getConnection()
->table($this->getTableName())
->where('identifier', $identifier)
->delete();
// Insert into the database with the new cart
$content = $this->getContent();
//dd($content);
$this
->getConnection()
->table($this->getTableName())
->insert([
'identifier' => $identifier,
'instance' => $this->currentInstance(),
'content' => serialize($content),
'created_at'=> new DateTime(),
]);
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
], $eventOptions);
$this->events->dispatch('cart.stored', [
$eventOptions,
]);
}
/**
* Restore the cart with the given identifier.
*
* @param $identifier
* @param array $eventOptions
* @return void
*/
public function restore($identifier, array $eventOptions = [], $delteStoredCart = true)
{
if ($this->storedCartWithIdentifierExists($identifier) === false) {
return;
}
// Find any existing carts by identifier
$stored = $this
->getConnection()
->table($this->getTableName())
->where('identifier', $identifier)
->first();
// Unserialize the content (either array if new, or collection if old)
$storedContent = unserialize($stored->content);
$currentInstance = $this->currentInstance();
$this->instance($stored->instance);
$content = $this->getContent();
// If the new approach and is array, set this class up.
// Note that it overrides any existing items in cart
// Does not add to existing.
if (is_array($storedContent)) {
$this->fromArray($storedContent);
}
// If the old approach and is Collection, push into existing items
if ($storedContent instanceof Collection) {
foreach ($storedContent as $cartItem) {
$content->put($cartItem->rowId, $cartItem);
}
}
$eventOptions = array_merge([
'cartInstance' => $this->currentInstance(),
], $eventOptions);
$this->events->dispatch('cart.restored', [
$eventOptions,
]);
$this->session->put($this->instance, $content);
$this->instance($currentInstance);
if($delteStoredCart === true) {
$this->deleteStoredCart($identifier);
}
}
/**
* Gets a specific fee from the fees array.
*
* @param $name
*
* @return mixed
*/
public function getFee($name)
{
return $this->fees->get($name, new CartFee(null, null));
}
/**
* Allows to charge for additional fees that may or may not be taxable
* ex - service fee , delivery fee, tips.
*
* Because it uses ->put, the name must be unique otherwise will be overwritten.
*
* @param $name
* @param $amount
* @param $taxRate
* @param array $options
*/
public function addFee($name, $amount, $taxRate = null, array $options = [])
{
$this->fees->put($name, new CartFee($amount, $taxRate, $options));
$this->session->put($this->instance, $this->toArray());
}
/**
* Removes a fee from the fee array.
*
* @todo test to see if i need to restore this
*
* @param $name
*/
public function removeFee($name)
{
$this->fees->forget($name);
$this->session->put($this->instance, $this->toArray());
}
/**
* Removes all the fees set in the cart.
*/
public function removeFees()
{
$this->fees = new Collection;
$this->session->put($this->instance, $this->toArray());
}
/**
* Gets all the fee totals.
*
* @param bool $format
* @param bool $withTax
*
* @return string
*/
public function feeTotal($decimals = null, $decimalPoint = null, $thousandSeperator = null, $withTax = true)
{
$feeTotal = 0;
foreach ($this->getFees() as $fee) {
$feeTotal += $fee->amount;
if ($withTax === true && $fee->taxRate > 0) {
$feeTotal += $fee->tax;
}
}
return $this->numberFormat($feeTotal, null, null, null);
}
/**
* Gets all the fees on the cart object.
*
* @return mixed
*/
public function getFees()
{
return $this->fees;
}
/**
* Magic method to make accessing the total, tax and subtotal properties possible.
*
* @param string $attribute
* @return float|null
*/
public function __get($attribute)
{
if ($attribute === 'total') {
return $this->total();
}
if ($attribute === 'feeTotal') {
return $this->feeTotal(null, null, null, false);
}
if ($attribute === 'feeTotalTax') {
return $this->feeTotal(null, null, null, true);
}
if ($attribute === 'tax') {
return $this->tax();
}
if ($attribute === 'feeTax') {
return $this->feeTax();
}
if ($attribute === 'subtotal') {
return $this->subtotal();
}
if ($attribute === 'subtotalTax') {
return $this->subtotalTax();
}
return null;
}
/**
* @return array
*/
public function toArray()
{
return [
'items' => $this->items,
'fees' => $this->fees,
];
}
/**
* @param $array
* @return $this
*/
public function fromArray($array)
{
$this->items = $array['items'];
$this->fees = $array['fees'];
return $this;
}
/**
* Deletes the stored cart with given identifier
*
* @param mixed $identifier
*/
public function deleteStoredCart($identifier)
{
$this
->getConnection()
->table($this->getTableName())
->where('identifier', $identifier)
->delete();
}
/**
* Get the carts content, if there is no cart content set yet, return a new empty Collection
*
* @return \Illuminate\Support\Collection
*/
public function getContent(): Collection
{
$instanceExists = $this->session->has($this->instance);
if ($instanceExists === false) {
$this->items = new Collection;
return $this->items;
}
$instance = $this->session->get($this->instance);
// dd($instance);
// If new approach, set $this variables
if (is_array($instance) === true) {
$this->items = $instance['items'];
$this->fees = $instance['fees'];
}
if ($instance instanceof Collection) {
$this->items = $instance;
}
return $this->items;
}
/**
* Create a new CartItem from the supplied attributes.
*
* @param mixed $id
* @param mixed $name
* @param int|float $qty
* @param float $price
* @param array $options
* @return \Gloudemans\Shoppingcart\CartItem
*/
private function createCartItem($id, $name, $qty, $price, $taxRate, bool $taxIncluded, array $options): CartItem
{
if ($id instanceof Buyable) {
$cartItem = CartItem::fromBuyable($id, $qty ?: []);
$cartItem->setQuantity($name ?: 1);
$cartItem->associate($id);
} elseif (is_array($id)) {
$cartItem = CartItem::fromArray($id);
$cartItem->setQuantity($id['qty']);
} else {
$cartItem = CartItem::fromAttributes($id, $name, $price, $options);
$cartItem->setQuantity($qty);
}
$taxRate = is_int($taxRate) ? $taxRate : config('cart.tax');
$cartItem->setTaxRate($taxRate);
$cartItem->setTaxIncluded($taxIncluded);
return $cartItem;
}
/**
* Check if the item is a multidimensional array or an array of Buyables.
*
* @param mixed $item
* @return bool
*/
private function isMulti($item): bool
{
if (is_array($item) === false) {
return false;
}
return is_array(head($item)) || head($item) instanceof Buyable === true;
}
/**
* @param $identifier
* @return bool
*/
protected function storedCartWithIdentifierExists($identifier): bool
{
return $this
->getConnection()
->table($this->getTableName())
->where('identifier', $identifier)
->exists();
}
/**
* Get the database connection.
*
* @return \Illuminate\Database\Connection
*/
protected function getConnection()
{
$connectionName = $this->getConnectionName();
return app(DatabaseManager::class)->connection($connectionName);
}
/**
* Get the database table name.
*
* @return string
*/
protected function getTableName()
{
return config('cart.database.table', 'shoppingcart');
}
/**
* Get the database connection name.
*
* @return string
*/
private function getConnectionName()
{
$connection = config('cart.database.connection');
return is_null($connection) ? config('database.default') : $connection;
}
}