diff --git a/.gitignore b/.gitignore index 38cdbd7..2c1fc0c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /vendor composer.phar composer.lock -.DS_Store -.idea \ No newline at end of file +.DS_Store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 2a71f20..5c8b48d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ php: - 5.4 - 5.5 - 5.6 + - 7.0 before_script: - composer self-update diff --git a/LICENSE b/LICENSE index 25c3bae..61c6afc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Rob Gloudemans +Copyright (c) 2016 Rob Gloudemans Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 508efb8..a1420f5 100644 --- a/README.md +++ b/README.md @@ -2,43 +2,25 @@ [![Build Status](https://travis-ci.org/Crinsane/LaravelShoppingcart.png?branch=master)](https://travis-ci.org/Crinsane/LaravelShoppingcart) [![Total Downloads](https://poser.pugx.org/gloudemans/shoppingcart/downloads.png)](https://packagist.org/packages/gloudemans/shoppingcart) -A simple shoppingcart implementation for Laravel 4. +A simple shoppingcart implementation for Laravel. ## Installation -Install the package through [Composer](http://getcomposer.org/). Edit your project's `composer.json` file by adding: +Install the package through [Composer](http://getcomposer.org/). -### Laravel 4.2 and below +Run the Composer require command from the Terminal: -```php -"require": { - "laravel/framework": "4.2.*", - "gloudemans/shoppingcart": "~1.2" -} -``` + composer require gloudemans/shoppingcart -### Laravel 5 - -```php -"require": { - "laravel/framework": "5.0.*", - "gloudemans/shoppingcart": "~1.3" -} -``` - -Next, run the Composer update command from the Terminal: - - composer update - -Now all you have to do is add the service provider of the package and alias the package. To do this open your `app/config/app.php` file. +Now all you have to do is add the service provider of the package and alias the package. To do this open your `config/app.php` file. Add a new line to the `service providers` array: - 'Gloudemans\Shoppingcart\ShoppingcartServiceProvider' + \Gloudemans\Shoppingcart\ShoppingcartServiceProvider::class -And finally add a new line to the `aliases` array: +And optionally add a new line to the `aliases` array: - 'Cart' => 'Gloudemans\Shoppingcart\Facades\Cart', + 'Cart' => \Gloudemans\Shoppingcart\Facades\Cart::class, Now you're ready to start using the shoppingcart in your application. diff --git a/composer.json b/composer.json index a41c1c2..d83c93c 100644 --- a/composer.json +++ b/composer.json @@ -1,29 +1,31 @@ { "name": "gloudemans/shoppingcart", - "description": "Laravel 5 Shoppingcart", + "description": "Laravel Shoppingcart", "keywords": ["laravel", "shoppingcart"], "license": "MIT", "authors": [ { "name": "Rob Gloudemans", - "email": "Rob_Gloudemans@hotmail.com" + "email": "info@robgloudemans.nl" } ], "require": { - "php": ">=5.4.0", - "illuminate/support": "~5.0" + "illuminate/support": "5.2.*", + "illuminate/session": "^5.2.*", + "illuminate/events": "^5.2.*" }, "require-dev": { - "mockery/mockery": "~0.9", - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "gloudemans/notify": "Simple flash notifications for Laravel 5" + "phpunit/phpunit": "~4.0", + "mockery/mockery": "~0.9.0", + "orchestra/testbench": "~3.0" }, "autoload": { - "psr-0": { - "Gloudemans\\Shoppingcart": "src/" + "psr-4": { + "Gloudemans\\Shoppingcart\\": "src/" } }, + "suggest": { + "gloudemans/notify": "Simple flash notifications for Laravel" + }, "minimum-stability": "dev" } diff --git a/config/cart.php b/config/cart.php new file mode 100644 index 0000000..da86159 --- /dev/null +++ b/config/cart.php @@ -0,0 +1,16 @@ + 21 + +]; \ No newline at end of file diff --git a/database/migrations/0000_00_00_000000_create_shoppingcart_table.php b/database/migrations/0000_00_00_000000_create_shoppingcart_table.php new file mode 100644 index 0000000..bebefa9 --- /dev/null +++ b/database/migrations/0000_00_00_000000_create_shoppingcart_table.php @@ -0,0 +1,30 @@ +string('identifier'); + $table->string('instance'); + $table->longText('content'); + $table->nullableTimestamps(); + + $table->primary(['identifier', 'instance']); + }); + } + /** + * Reverse the migrations. + */ + public function down() + { + Schema::drop('shoppingcart'); + } +} diff --git a/phpunit.xml b/phpunit.xml index e89ac6d..f994f39 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -8,8 +8,7 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" -> + syntaxCheck="false"> ./tests/ diff --git a/src/CanBeBought.php b/src/CanBeBought.php new file mode 100644 index 0000000..4214509 --- /dev/null +++ b/src/CanBeBought.php @@ -0,0 +1,43 @@ +getKey() : $this->id; + } + + /** + * Get the description or title of the Buyable item. + * + * @return string + */ + public function getBuyableDescription() + { + if(property_exists('name', $this)) return $this->name; + if(property_exists('title', $this)) return $this->title; + if(property_exists('description', $this)) return $this->description; + + return null; + } + + /** + * Get the price of the Buyable item. + * + * @return float + */ + public function getBuyablePrice() + { + if(property_exists('price', $this)) return $this->price; + + return null; + } +} \ No newline at end of file diff --git a/src/Cart.php b/src/Cart.php new file mode 100644 index 0000000..0d24dde --- /dev/null +++ b/src/Cart.php @@ -0,0 +1,503 @@ +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 mixed $id + * @param mixed $name + * @param int|float $qty + * @param float $price + * @param array $options + * @return \Gloudemans\Shoppingcart\CartItem + */ + public function add($id, $name = null, $qty = null, $price = null, array $options = []) + { + if ($this->isMulti($id)) { + return array_map(function ($item) { + return $this->add($item); + }, $id); + } + + $cartItem = $this->createCartItem($id, $name, $qty, $price, $options); + + $content = $this->getContent(); + + if ($content->has($cartItem->rowId)) { + $cartItem->qty++; + } + + $content->put($cartItem->rowId, $cartItem); + + $this->events->fire('cart.added', $cartItem); + + $this->session->put($this->instance, $content); + + return $cartItem; + } + + /** + * Update the cart item with the given rowId. + * + * @param string $rowId + * @param mixed $qty + * @return void + */ + public function update($rowId, $qty) + { + $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) { + $content->pull($rowId); + + if ($content->has($cartItem->rowId)) { + $existingCartItem = $this->get($cartItem->rowId); + $cartItem->setQuantity($existingCartItem->qty + $cartItem->qty); + } + } + + if ($cartItem->qty <= 0) { + $this->remove($cartItem->rowId); + return; + } else { + $content->put($cartItem->rowId, $cartItem); + } + + $this->events->fire('cart.updated', $cartItem); + + $this->session->put($this->instance, $content); + } + + /** + * Remove the cart item with the given rowId from the cart. + * + * @param string $rowId + * @return void + */ + public function remove($rowId) + { + $cartItem = $this->get($rowId); + + $content = $this->getContent(); + + $content->pull($cartItem->rowId); + + $this->events->fire('cart.removed', $cartItem); + + $this->session->put($this->instance, $content); + } + + /** + * 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)) + 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->session->get($this->instance); + } + + /** + * 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 float + */ + public function total($decimals = 2, $decimalPoint = '.', $thousandSeperator = ',') + { + $content = $this->getContent(); + + $total = $content->reduce(function ($subTotal, CartItem $cartItem) { + return $subTotal + ($cartItem->qty * $cartItem->price); + }, 0); + + return number_format($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 = 2, $decimalPoint = '.', $thousandSeperator = ',') + { + $content = $this->getContent(); + + $tax = $content->reduce(function ($tax, CartItem $cartItem) { + return $tax + ($cartItem->qty * $cartItem->tax); + }, 0); + + return number_format($tax, $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 subtotal($decimals = 2, $decimalPoint = '.', $thousandSeperator = ',') + { + $subtotal = $this->total - $this->tax; + + return number_format($subtotal, $decimals, $decimalPoint, $thousandSeperator); + } + + /** + * Search the cart content for a cart item matching the given search closure. + * + * @param \Closure $search + * @return \Gloudemans\Shoppingcart\CartItem|\Illuminate\Support\Collection + */ + public function search(Closure $search) + { + $content = $this->getContent(); + + $found = $content->filter($search); + + if($found->count() === 1) return $found->first(); + + return $found; + } + + /** + * 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) && ! class_exists($model)) + 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, $content); + } + + /** + * 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->session->put($this->instance, $content); + } + + /** + * Store an the current instance of the cart. + * + * @param mixed $identifier + * @return void + */ + public function store($identifier) + { + $content = $this->getContent(); + + if ($this->storedCartWithIdentifierExists($identifier)) { + throw new CartAlreadyStoredException("A cart with identifier {$identifier} was already stored."); + } + + $this->getConnection()->table($this->getTableName())->insert([ + 'identifier' => $identifier, + 'instance' => $this->currentInstance(), + 'content' => serialize($content) + ]); + + $this->events->fire('cart.stored'); + } + + /** + * Restore the cart with the given identifier. + * + * @param mixed $identifier + * @return void + */ + public function restore($identifier) + { + if( ! $this->storedCartWithIdentifierExists($identifier)) { + return; + } + + $stored = $this->getConnection()->table($this->getTableName()) + ->where('identifier', $identifier)->first(); + + $storedContent = unserialize($stored->content); + + $currentInstance = $this->currentInstance(); + + $this->instance($stored->instance); + + $content = $this->getContent(); + + foreach ($storedContent as $cartItem) { + $content->put($cartItem->rowId, $cartItem); + } + + $this->events->fire('cart.restored'); + + $this->session->put($this->instance, $content); + + $this->instance($currentInstance); + + $this->getConnection()->table($this->getTableName()) + ->where('identifier', $identifier)->delete(); + } + + /** + * 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(2, '.', ''); + } + + if($attribute === 'tax') { + return $this->tax(2, '.', ''); + } + + if($attribute === 'subtotal') { + return $this->subtotal(2, '.', ''); + } + + return null; + } + + /** + * Get the carts content, if there is no cart content set yet, return a new empty Collection + * + * @return \Illuminate\Support\Collection + */ + protected function getContent() + { + $content = $this->session->has($this->instance) + ? $this->session->get($this->instance) + : new Collection; + + return $content; + } + + /** + * 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, array $options) + { + 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); + } + + $cartItem->setTaxRate(config('cart.tax')); + + return $cartItem; + } + + /** + * Check if the item is a multidimensional array or an array of Buyables. + * + * @param mixed $item + * @return bool + */ + private function isMulti($item) + { + if ( ! is_array($item)) return false; + + return is_array(head($item)) || head($item) instanceof Buyable; + } + + /** + * @param $identifier + * @return bool + */ + private function storedCartWithIdentifierExists($identifier) + { + return $this->getConnection()->table($this->getTableName())->where('identifier', $identifier)->exists(); + } + + /** + * Get the database connection. + * + * @return \Illuminate\Database\Connection + */ + private function getConnection() + { + return app(DatabaseManager::class)->connection(config('cart.database.connection', config('database.default'))); + } + + /** + * Get the database table name. + * + * @return string + */ + private function getTableName() + { + return config('cart.database.table', 'shoppingcart'); + } +} \ No newline at end of file diff --git a/src/CartItem.php b/src/CartItem.php new file mode 100644 index 0000000..1ef6cbe --- /dev/null +++ b/src/CartItem.php @@ -0,0 +1,271 @@ +id = $id; + $this->name = $name; + $this->price = $price; + $this->options = new CartItemOptions($options); + + $this->rowId = $this->generateRowId($id, $options); + } + + /** + * Returns the formatted price. + * + * @param int $decimals + * @param string $decimalPoint + * @param string $thousandSeperator + * @return string + */ + public function price($decimals = 2, $decimalPoint = '.', $thousandSeperator = ',') + { + return number_format($this->price, $decimals, $decimalPoint, $thousandSeperator); + } + + /** + * Returns the formatted subtotal. + * + * @param int $decimals + * @param string $decimalPoint + * @param string $thousandSeperator + * @return string + */ + public function subtotal($decimals = 2, $decimalPoint = '.', $thousandSeperator = ',') + { + return number_format($this->subtotal, $decimals, $decimalPoint, $thousandSeperator); + } + + /** + * Returns the formatted tax. + * + * @param int $decimals + * @param string $decimalPoint + * @param string $thousandSeperator + * @return string + */ + public function tax($decimals = 2, $decimalPoint = '.', $thousandSeperator = ',') + { + return number_format($this->tax, $decimals, $decimalPoint, $thousandSeperator); + } + + /** + * Set the quantity for this cart item. + * + * @param int|float $qty + */ + public function setQuantity($qty) + { + if(empty($qty) || ! is_numeric($qty)) + throw new \InvalidArgumentException('Please supply a valid quantity.'); + + $this->qty = $qty; + } + + /** + * Update the cart item from a Buyable. + * + * @param \Gloudemans\Shoppingcart\Contracts\Buyable $item + * @return void + */ + public function updateFromBuyable(Buyable $item) + { + $this->id = $item->getBuyableIdentifier(); + $this->name = $item->getBuyableDescription(); + $this->price = $item->getBuyablePrice(); + } + + /** + * Update the cart item from an array. + * + * @param array $attributes + * @return void + */ + public function updateFromArray(array $attributes) + { + $this->id = array_get($attributes, 'id', $this->id); + $this->qty = array_get($attributes, 'qty', $this->qty); + $this->name = array_get($attributes, 'name', $this->name); + $this->price = array_get($attributes, 'price', $this->price); + $this->options = new CartItemOptions(array_get($attributes, 'options', [])); + + $this->rowId = $this->generateRowId($this->id, $this->options->all()); + } + + /** + * Associate the cart item with the given model. + * + * @param mixed $model + * @return void + */ + public function associate($model) + { + $this->associatedModel = is_string($model) ? $model : get_class($model); + } + + /** + * Set the tax rate. + * + * @param int|float $taxRate + * @return void + */ + public function setTaxRate($taxRate) + { + $this->taxRate = $taxRate; + } + + /** + * Get an attribute from the cart item or get the associated model. + * + * @param string $attribute + * @return mixed + */ + public function __get($attribute) + { + if(property_exists($this, $attribute)) { + return $this->{$attribute}; + } + + if($attribute === 'subtotal') { + return $this->qty * $this->price; + } + + if($attribute === 'tax') { + return $this->price * ($this->taxRate / 100); + } + + if($attribute === 'model') { + return with(new $this->associatedModel)->find($this->id); + } + + return null; + } + + /** + * Create a new instance from a Buyable. + * + * @param \Gloudemans\Shoppingcart\Contracts\Buyable $item + * @param array $options + * @return \Gloudemans\Shoppingcart\CartItem + */ + public static function fromBuyable(Buyable $item, array $options = []) + { + return new self($item->getBuyableIdentifier(), $item->getBuyableDescription(), $item->getBuyablePrice(), $options); + } + + /** + * Create a new instance from the given array. + * + * @param array $attributes + * @return \Gloudemans\Shoppingcart\CartItem + */ + public static function fromArray(array $attributes) + { + $options = array_get($attributes, 'options', []); + + return new self($attributes['id'], $attributes['name'], $attributes['price'], $options); + } + + /** + * Create a new instance from the given attributes. + * + * @param int|string $id + * @param string $name + * @param float $price + * @param array $options + * @return \Gloudemans\Shoppingcart\CartItem + */ + public static function fromAttributes($id, $name, $price, array $options = []) + { + return new self($id, $name, $price, $options); + } + + /** + * Generate a unique id for the cart item. + * + * @param string $id + * @param array $options + * @return string + */ + protected function generateRowId($id, array $options) + { + ksort($options); + + return md5($id . serialize($options)); + } +} \ No newline at end of file diff --git a/src/CartItemOptions.php b/src/CartItemOptions.php new file mode 100644 index 0000000..bbd24e9 --- /dev/null +++ b/src/CartItemOptions.php @@ -0,0 +1,19 @@ +get($key); + } +} \ No newline at end of file diff --git a/src/Contracts/Buyable.php b/src/Contracts/Buyable.php new file mode 100644 index 0000000..9077e37 --- /dev/null +++ b/src/Contracts/Buyable.php @@ -0,0 +1,27 @@ +session = $session; - $this->event = $event; - - $this->instance = 'main'; - } - - /** - * Set the current cart instance - * - * @param string $instance Cart instance name - * @return Gloudemans\Shoppingcart\Cart - */ - public function instance($instance = null) - { - if(empty($instance)) throw new Exceptions\ShoppingcartInstanceException; - - $this->instance = $instance; - - // Return self so the method is chainable - return $this; - } - - /** - * Set the associated model - * - * @param string $modelName The name of the model - * @param string $modelNamespace The namespace of the model - * @return void - */ - public function associate($modelName, $modelNamespace = null) - { - $this->associatedModel = $modelName; - $this->associatedModelNamespace = $modelNamespace; - - if( ! class_exists($modelNamespace . '\\' . $modelName)) throw new Exceptions\ShoppingcartUnknownModelException; - - // Return self so the method is chainable - return $this; - } - - /** - * Add a row to the cart - * - * @param string|array $id Unique ID of the item|Item formated as array|Array of items - * @param string $name Name of the item - * @param int $qty Item qty to add to the cart - * @param float $price Price of one item - * @param array $options Array of additional options, such as 'size' or 'color' - */ - public function add($id, $name = null, $qty = null, $price = null, array $options = []) - { - // If the first parameter is an array we need to call the add() function again - if(is_array($id)) - { - // And if it's not only an array, but a multidimensional array, we need to - // recursively call the add function - if($this->is_multi($id)) - { - // Fire the cart.batch event - $this->event->fire('cart.batch', $id); - - foreach($id as $item) - { - $options = array_get($item, 'options', []); - $this->addRow($item['id'], $item['name'], $item['qty'], $item['price'], $options); - } - - // Fire the cart.batched event - $this->event->fire('cart.batched', $id); - - return; - } - - $options = array_get($id, 'options', []); - - // Fire the cart.add event - $this->event->fire('cart.add', array_merge($id, ['options' => $options])); - - $result = $this->addRow($id['id'], $id['name'], $id['qty'], $id['price'], $options); - - // Fire the cart.added event - $this->event->fire('cart.added', array_merge($id, ['options' => $options])); - - return $result; - } - - // Fire the cart.add event - $this->event->fire('cart.add', compact('id', 'name', 'qty', 'price', 'options')); - - $result = $this->addRow($id, $name, $qty, $price, $options); - - // Fire the cart.added event - $this->event->fire('cart.added', compact('id', 'name', 'qty', 'price', 'options')); - - return $result; - } - - /** - * Update the quantity of one row of the cart - * - * @param string $rowId The rowid of the item you want to update - * @param integer|array $attribute New quantity of the item|Array of attributes to update - * @return boolean - */ - public function update($rowId, $attribute) - { - if( ! $this->hasRowId($rowId)) throw new Exceptions\ShoppingcartInvalidRowIDException; - - if(is_array($attribute)) - { - // Fire the cart.update event - $this->event->fire('cart.update', $rowId); - - $result = $this->updateAttribute($rowId, $attribute); - - // Fire the cart.updated event - $this->event->fire('cart.updated', $rowId); - - return $result; - } - - // Fire the cart.update event - $this->event->fire('cart.update', $rowId); - - $result = $this->updateQty($rowId, $attribute); - - // Fire the cart.updated event - $this->event->fire('cart.updated', $rowId); - - return $result; - } - - /** - * Remove a row from the cart - * - * @param string $rowId The rowid of the item - * @return boolean - */ - public function remove($rowId) - { - if( ! $this->hasRowId($rowId)) throw new Exceptions\ShoppingcartInvalidRowIDException; - - $cart = $this->getContent(); - - // Fire the cart.remove event - $this->event->fire('cart.remove', $rowId); - - $cart->forget($rowId); - - // Fire the cart.removed event - $this->event->fire('cart.removed', $rowId); - - return $this->updateCart($cart); - } - - /** - * Get a row of the cart by its ID - * - * @param string $rowId The ID of the row to fetch - * @return Gloudemans\Shoppingcart\CartCollection - */ - public function get($rowId) - { - $cart = $this->getContent(); - - return ($cart->has($rowId)) ? $cart->get($rowId) : NULL; - } - - /** - * Get the cart content - * - * @return Gloudemans\Shoppingcart\CartRowCollection - */ - public function content() - { - $cart = $this->getContent(); - - return (empty($cart)) ? NULL : $cart; - } - - /** - * Empty the cart - * - * @return boolean - */ - public function destroy() - { - // Fire the cart.destroy event - $this->event->fire('cart.destroy'); - - $result = $this->updateCart(NULL); - - // Fire the cart.destroyed event - $this->event->fire('cart.destroyed'); - - return $result; - } - - /** - * Get the price total - * - * @return float - */ - public function total() - { - $total = 0; - $cart = $this->getContent(); - - if(empty($cart)) - { - return $total; - } - - foreach($cart AS $row) - { - $total += $row->subtotal; - } - - return $total; - } - - /** - * Get the number of items in the cart - * - * @param boolean $totalItems Get all the items (when false, will return the number of rows) - * @return int - */ - public function count($totalItems = true) - { - $cart = $this->getContent(); - - if( ! $totalItems) - { - return $cart->count(); - } - - $count = 0; - - foreach($cart AS $row) - { - $count += $row->qty; - } - - return $count; - } - - /** - * Search if the cart has a item - * - * @param array $search An array with the item ID and optional options - * @return array|boolean - */ - public function search(array $search) - { - if(empty($search)) return false; - - foreach($this->getContent() as $item) - { - $found = $item->search($search); - - if($found) - { - $rows[] = $item->rowid; - } - } - - return (empty($rows)) ? false : $rows; - } - - /** - * Add row to the cart - * - * @param string $id Unique ID of the item - * @param string $name Name of the item - * @param int $qty Item qty to add to the cart - * @param float $price Price of one item - * @param array $options Array of additional options, such as 'size' or 'color' - */ - protected function addRow($id, $name, $qty, $price, array $options = []) - { - if(empty($id) || empty($name) || empty($qty) || ! isset($price)) - { - throw new Exceptions\ShoppingcartInvalidItemException; - } - - if( ! is_numeric($qty)) - { - throw new Exceptions\ShoppingcartInvalidQtyException; - } - - if( ! is_numeric($price)) - { - throw new Exceptions\ShoppingcartInvalidPriceException; - } - - $cart = $this->getContent(); - - $rowId = $this->generateRowId($id, $options); - - if($cart->has($rowId)) - { - $row = $cart->get($rowId); - $cart = $this->updateRow($rowId, ['qty' => $row->qty + $qty]); - } - else - { - $cart = $this->createRow($rowId, $id, $name, $qty, $price, $options); - } - - return $this->updateCart($cart); - } - - /** - * Generate a unique id for the new row - * - * @param string $id Unique ID of the item - * @param array $options Array of additional options, such as 'size' or 'color' - * @return boolean - */ - protected function generateRowId($id, $options) - { - ksort($options); - - return md5($id . serialize($options)); - } - - /** - * Check if a rowid exists in the current cart instance - * - * @param string $id Unique ID of the item - * @return boolean - */ - protected function hasRowId($rowId) - { - return $this->getContent()->has($rowId); - } - - /** - * Update the cart - * - * @param Gloudemans\Shoppingcart\CartCollection $cart The new cart content - * @return void - */ - protected function updateCart($cart) - { - return $this->session->put($this->getInstance(), $cart); - } - - /** - * Get the carts content, if there is no cart content set yet, return a new empty Collection - * - * @return Gloudemans\Shoppingcart\CartCollection - */ - protected function getContent() - { - $content = ($this->session->has($this->getInstance())) ? $this->session->get($this->getInstance()) : new CartCollection; - - return $content; - } - - /** - * Get the current cart instance - * - * @return string - */ - protected function getInstance() - { - return 'cart.' . $this->instance; - } - - /** - * Update a row if the rowId already exists - * - * @param string $rowId The ID of the row to update - * @param integer $qty The quantity to add to the row - * @return Gloudemans\Shoppingcart\CartCollection - */ - protected function updateRow($rowId, $attributes) - { - $cart = $this->getContent(); - - $row = $cart->get($rowId); - - foreach($attributes as $key => $value) - { - if($key == 'options') - { - $options = $row->options->merge($value); - $row->put($key, $options); - } - else - { - $row->put($key, $value); - } - } - - if( ! is_null(array_keys($attributes, ['qty', 'price']))) - { - $row->put('subtotal', $row->qty * $row->price); - } - - $cart->put($rowId, $row); - - return $cart; - } - - /** - * Create a new row Object - * - * @param string $rowId The ID of the new row - * @param string $id Unique ID of the item - * @param string $name Name of the item - * @param int $qty Item qty to add to the cart - * @param float $price Price of one item - * @param array $options Array of additional options, such as 'size' or 'color' - * @return Gloudemans\Shoppingcart\CartCollection - */ - protected function createRow($rowId, $id, $name, $qty, $price, $options) - { - $cart = $this->getContent(); - - $newRow = new CartRowCollection([ - 'rowid' => $rowId, - 'id' => $id, - 'name' => $name, - 'qty' => $qty, - 'price' => $price, - 'options' => new CartRowOptionsCollection($options), - 'subtotal' => $qty * $price - ], $this->associatedModel, $this->associatedModelNamespace); - - $cart->put($rowId, $newRow); - - return $cart; - } - - /** - * Update the quantity of a row - * - * @param string $rowId The ID of the row - * @param int $qty The qty to add - * @return Gloudemans\Shoppingcart\CartCollection - */ - protected function updateQty($rowId, $qty) - { - if($qty <= 0) - { - return $this->remove($rowId); - } - - return $this->updateRow($rowId, ['qty' => $qty]); - } - - /** - * Update an attribute of the row - * - * @param string $rowId The ID of the row - * @param array $attributes An array of attributes to update - * @return Gloudemans\Shoppingcart\CartCollection - */ - protected function updateAttribute($rowId, $attributes) - { - return $this->updateRow($rowId, $attributes); - } - - /** - * Check if the array is a multidimensional array - * - * @param array $array The array to check - * @return boolean - */ - protected function is_multi(array $array) - { - return is_array(head($array)); - } - -} diff --git a/src/Gloudemans/Shoppingcart/CartCollection.php b/src/Gloudemans/Shoppingcart/CartCollection.php deleted file mode 100644 index c825aca..0000000 --- a/src/Gloudemans/Shoppingcart/CartCollection.php +++ /dev/null @@ -1,7 +0,0 @@ -associatedModel = $associatedModel; - $this->associatedModelNamespace = $associatedModelNamespace; - } - - public function __get($arg) - { - if($this->has($arg)) - { - return $this->get($arg); - } - - if($arg == strtolower($this->associatedModel)) - { - $modelInstance = $this->associatedModelNamespace ? $this->associatedModelNamespace . '\\' .$this->associatedModel : $this->associatedModel; - $model = new $modelInstance; - - return $model->find($this->id); - } - - return null; - } - - public function search($search, $strict = false) - { - foreach($search as $key => $value) - { - if($key === 'options') - { - $found = $this->{$key}->search($value); - } - else - { - $found = ($this->{$key} === $value) ? true : false; - } - - if( ! $found) return false; - } - - return $found; - } - -} \ No newline at end of file diff --git a/src/Gloudemans/Shoppingcart/CartRowOptionsCollection.php b/src/Gloudemans/Shoppingcart/CartRowOptionsCollection.php deleted file mode 100644 index a384069..0000000 --- a/src/Gloudemans/Shoppingcart/CartRowOptionsCollection.php +++ /dev/null @@ -1,34 +0,0 @@ -has($arg)) - { - return $this->get($arg); - } - - return NULL; - } - - public function search($search, $strict = false) - { - foreach($search as $key => $value) - { - $found = ($this->{$key} === $value) ? true : false; - - if( ! $found) return false; - } - - return $found; - } - -} \ No newline at end of file diff --git a/src/Gloudemans/Shoppingcart/Exceptions/ShoppingcartInstanceException.php b/src/Gloudemans/Shoppingcart/Exceptions/ShoppingcartInstanceException.php deleted file mode 100644 index 17e2bf0..0000000 --- a/src/Gloudemans/Shoppingcart/Exceptions/ShoppingcartInstanceException.php +++ /dev/null @@ -1,3 +0,0 @@ -app['cart'] = $this->app->share(function($app) - { - $session = $app['session']; - $events = $app['events']; - return new Cart($session, $events); - }); - } -} \ No newline at end of file diff --git a/src/ShoppingcartServiceProvider.php b/src/ShoppingcartServiceProvider.php new file mode 100644 index 0000000..da02981 --- /dev/null +++ b/src/ShoppingcartServiceProvider.php @@ -0,0 +1,29 @@ +mergeConfigFrom($config, 'cart'); + + if ( ! class_exists('CreateShoppingcartTable')) { + // Publish the migration + $timestamp = date('Y_m_d_His', time()); + + $this->publishes([ + __DIR__.'/../database/migrations/0000_00_00_000000_create_shoppingcart_table.php' => database_path('migrations/'.$timestamp.'_create_shoppingcart_table.php'), + ], 'migrations'); + } + } +} \ No newline at end of file diff --git a/tests/.gitkeep b/tests/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/CartAssertions.php b/tests/CartAssertions.php new file mode 100644 index 0000000..69b0211 --- /dev/null +++ b/tests/CartAssertions.php @@ -0,0 +1,35 @@ +count(); + + PHPUnit::assertEquals($items, $cart->count(), "Expected the cart to contain {$items} items, but got {$actual}."); + } + + /** + * Assert that the cart contains the given number of rows. + * + * @param int $rows + * @param \Gloudemans\Shoppingcart\Cart $cart + */ + public function assertRowsInCart($rows, Cart $cart) + { + $actual = $cart->content()->count(); + + PHPUnit::assertCount($rows, $cart->content(), "Expected the cart to contain {$rows} rows, but got {$actual}."); + } + +} \ No newline at end of file diff --git a/tests/CartTest.php b/tests/CartTest.php index 5d70535..8d624e0 100644 --- a/tests/CartTest.php +++ b/tests/CartTest.php @@ -1,425 +1,857 @@ events = m::mock('Illuminate\Contracts\Events\Dispatcher'); - - $this->cart = new Cart($session, $this->events); - } - - public function tearDown() - { - m::close(); - } - - public function testCartCanAdd() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99, array('size' => 'large')); - } - - public function testCartCanAddMultiple() + /** + * Set the package service provider. + * + * @param \Illuminate\Foundation\Application $app + * @return array + */ + protected function getPackageProviders($app) { - $this->events->shouldReceive('fire')->times(5)->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->times(5)->with('cart.added', m::type('array')); - - for($i = 1; $i <= 5; $i++) - { - $this->cart->add('293ad' . $i, 'Product ' . $i, 1, 9.99); - } - - $this->assertEquals(5, $this->cart->count()); + return [\Gloudemans\Shoppingcart\ShoppingcartServiceProvider::class]; } - public function testCartCanAddWithNumericId() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - - $this->cart->add(12345, 'Product 1', 1, 9.99, array('size' => 'large')); - } - - public function testCartCanAddArray() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - - $this->cart->add(array('id' => '293ad', 'name' => 'Product 1', 'qty' => 1, 'price' => 9.99, 'options' => array('size' => 'large'))); - } - - public function testCartCanAddBatch() - { - $this->events->shouldReceive('fire')->once()->with('cart.batch', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.batched', m::type('array')); - - $this->cart->add(array( - array('id' => '293ad', 'name' => 'Product 1', 'qty' => 1, 'price' => 10.00), - array('id' => '4832k', 'name' => 'Product 2', 'qty' => 1, 'price' => 10.00, 'options' => array('size' => 'large')) - )); - } - - public function testCartCanAddMultipleOptions() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99, array('size' => 'large', 'color' => 'red')); - - $cartRow = $this->cart->get('c5417b5761c7fb837e4227a38870dd4d'); - - $this->assertInstanceOf('Gloudemans\Shoppingcart\CartRowOptionsCollection', $cartRow->options); - $this->assertEquals('large', $cartRow->options->size); - $this->assertEquals('red', $cartRow->options->color); - } - - /** - * @expectedException Gloudemans\Shoppingcart\Exceptions\ShoppingcartInvalidItemException - */ - public function testCartThrowsExceptionOnEmptyItem() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::any()); - - $this->cart->add('', '', '', ''); - } - - /** - * @expectedException Gloudemans\Shoppingcart\Exceptions\ShoppingcartInvalidQtyException - */ - public function testCartThrowsExceptionOnNoneNumericQty() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::any()); - - $this->cart->add('293ad', 'Product 1', 'none-numeric', 9.99); - } - - /** - * @expectedException Gloudemans\Shoppingcart\Exceptions\ShoppingcartInvalidPriceException - */ - public function testCartThrowsExceptionOnNoneNumericPrice() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::any()); - - $this->cart->add('293ad', 'Product 1', 1, 'none-numeric'); - } - - public function testCartCanUpdateExistingItem() - { - $this->events->shouldReceive('fire')->twice()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->twice()->with('cart.added', m::type('array')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $this->cart->add('293ad', 'Product 1', 1, 9.99); - - $this->assertEquals(2, $this->cart->content()->first()->qty); - } - - public function testCartCanUpdateQty() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.update', m::type('string')); - $this->events->shouldReceive('fire')->once()->with('cart.updated', m::type('string')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $this->cart->update('8cbf215baa3b757e910e5305ab981172', 2); - - $this->assertEquals(2, $this->cart->content()->first()->qty); - } - - public function testCartCanUpdateItem() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.update', m::type('string')); - $this->events->shouldReceive('fire')->once()->with('cart.updated', m::type('string')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $this->cart->update('8cbf215baa3b757e910e5305ab981172', array('name' => 'Product 2')); - - $this->assertEquals('Product 2', $this->cart->content()->first()->name); - } - - public function testCartCanUpdateItemToNumericId() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.update', m::type('string')); - $this->events->shouldReceive('fire')->once()->with('cart.updated', m::type('string')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $this->cart->update('8cbf215baa3b757e910e5305ab981172', array('id' => 12345)); - - $this->assertEquals(12345, $this->cart->content()->first()->id); - } - - public function testCartCanUpdateOptions() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.update', m::type('string')); - $this->events->shouldReceive('fire')->once()->with('cart.updated', m::type('string')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99, array('size' => 'S')); - $this->cart->update('9be7e69d236ca2d09d2e0838d2c59aeb', array('options' => array('size' => 'L'))); - - $this->assertEquals('L', $this->cart->content()->first()->options->size); - } - - /** - * @expectedException Gloudemans\Shoppingcart\Exceptions\ShoppingcartInvalidRowIDException - */ - public function testCartThrowsExceptionOnInvalidRowId() - { - $this->cart->update('invalidRowId', 1); - } - - public function testCartCanRemove() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.remove', m::type('string')); - $this->events->shouldReceive('fire')->once()->with('cart.removed', m::type('string')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $this->cart->remove('8cbf215baa3b757e910e5305ab981172'); - - $this->assertTrue($this->cart->content()->isEmpty()); - } - - public function testCartCanRemoveOnUpdate() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.update', m::type('string')); - $this->events->shouldReceive('fire')->once()->with('cart.updated', m::type('string')); - $this->events->shouldReceive('fire')->once()->with('cart.remove', m::type('string')); - $this->events->shouldReceive('fire')->once()->with('cart.removed', m::type('string')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $this->cart->update('8cbf215baa3b757e910e5305ab981172', 0); - - $this->assertTrue($this->cart->content()->isEmpty()); - } - - public function testCartCanRemoveOnNegativeUpdate() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.update', m::type('string')); - $this->events->shouldReceive('fire')->once()->with('cart.updated', m::type('string')); - $this->events->shouldReceive('fire')->once()->with('cart.remove', m::type('string')); - $this->events->shouldReceive('fire')->once()->with('cart.removed', m::type('string')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $this->cart->update('8cbf215baa3b757e910e5305ab981172', -1); - - $this->assertTrue($this->cart->content()->isEmpty()); - } - - public function testCartCanGet() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $item = $this->cart->get('8cbf215baa3b757e910e5305ab981172'); - - $this->assertEquals('293ad', $item->id); - } - - public function testCartCanGetContent() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - - $this->assertInstanceOf('Gloudemans\Shoppingcart\CartCollection', $this->cart->content()); - $this->assertFalse($this->cart->content()->isEmpty()); - } - - public function testCartCanDestroy() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.destroy'); - $this->events->shouldReceive('fire')->once()->with('cart.destroyed'); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $this->cart->destroy(); - - $this->assertInstanceOf('Gloudemans\Shoppingcart\CartCollection', $this->cart->content()); - $this->assertTrue($this->cart->content()->isEmpty()); - } - - public function testCartCanGetTotal() - { - $this->events->shouldReceive('fire')->twice()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->twice()->with('cart.added', m::type('array')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $this->cart->add('986se', 'Product 2', 1, 19.99); - - $this->assertEquals(29.98, $this->cart->total()); - } - - public function testCartCanGetItemCount() - { - $this->events->shouldReceive('fire')->twice()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->twice()->with('cart.added', m::type('array')); - - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $this->cart->add('986se', 'Product 2', 2, 19.99); + /** + * Define environment setup. + * + * @param \Illuminate\Foundation\Application $app + * @return void + */ + protected function getEnvironmentSetUp($app) + { + $app['config']->set('cart.database.connection', 'testing'); - $this->assertEquals(3, $this->cart->count()); - } - - public function testCartCanGetRowCount() - { - $this->events->shouldReceive('fire')->twice()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->twice()->with('cart.added', m::type('array')); + $app['config']->set('session.driver', 'array'); - $this->cart->add('293ad', 'Product 1', 1, 9.99); - $this->cart->add('986se', 'Product 2', 2, 19.99); + $app['config']->set('database.default', 'testing'); + $app['config']->set('database.connections.testing', [ + 'driver' => 'sqlite', + 'database' => ':memory:', + 'prefix' => '', + ]); + } + + /** @test */ + public function it_has_a_default_instance() + { + $cart = $this->getCart(); - $this->assertEquals(2, $this->cart->count(false)); - } + $this->assertEquals(Cart::DEFAULT_INSTANCE, $cart->currentInstance()); + } - public function testCartCanSearch() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); + /** @test */ + public function it_can_have_multiple_instances() + { + $cart = $this->getCart(); - $this->cart->add('293ad', 'Product 1', 1, 9.99); + $item = $this->getBuyableMock(1, 'First item'); - $searchResult = $this->cart->search(array('id' => '293ad')); - $this->assertEquals('8cbf215baa3b757e910e5305ab981172', $searchResult[0]); - } + $cart->add($item); - public function testCartCanHaveMultipleInstances() - { - $this->events->shouldReceive('fire')->twice()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->twice()->with('cart.added', m::type('array')); + $item2 = $this->getBuyableMock(2, 'Second item'); - $this->cart->instance('firstInstance')->add('293ad', 'Product 1', 1, 9.99); - $this->cart->instance('secondInstance')->add('986se', 'Product 2', 1, 19.99); + $cart->instance('wishlist')->add($item2); - $this->assertTrue($this->cart->instance('firstInstance')->content()->has('8cbf215baa3b757e910e5305ab981172')); - $this->assertFalse($this->cart->instance('firstInstance')->content()->has('22eae2b9c10083d6631aaa023106871a')); - $this->assertTrue($this->cart->instance('secondInstance')->content()->has('22eae2b9c10083d6631aaa023106871a')); - $this->assertFalse($this->cart->instance('secondInstance')->content()->has('8cbf215baa3b757e910e5305ab981172')); - } + $this->assertItemsInCart(1, $cart->instance(Cart::DEFAULT_INSTANCE)); + $this->assertItemsInCart(1, $cart->instance('wishlist')); + } + + /** @test */ + public function it_can_add_an_item() + { + $this->expectsEvents('cart.added'); - public function testCartCanSearchInMultipleInstances() - { - $this->events->shouldReceive('fire')->twice()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->twice()->with('cart.added', m::type('array')); + $cart = $this->getCart(); - $this->cart->instance('firstInstance')->add('293ad', 'Product 1', 1, 9.99); - $this->cart->instance('secondInstance')->add('986se', 'Product 2', 1, 19.99); + $item = $this->getBuyableMock(); - $this->assertEquals($this->cart->instance('firstInstance')->search(array('id' => '293ad')), array('8cbf215baa3b757e910e5305ab981172')); - $this->assertEquals($this->cart->instance('secondInstance')->search(array('id' => '986se')), array('22eae2b9c10083d6631aaa023106871a')); - } + $cart->add($item); - /** - * @expectedException Gloudemans\Shoppingcart\Exceptions\ShoppingcartInstanceException - */ - public function testCartThrowsExceptionOnEmptyInstance() - { - $this->cart->instance(); - } + $this->assertEquals(1, $cart->count()); + } - public function testCartReturnsCartCollection() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); + /** @test */ + public function it_will_return_the_cartitem_of_the_added_item() + { + $this->expectsEvents('cart.added'); - $this->cart->add('293ad', 'Product 1', 1, 9.99); + $cart = $this->getCart(); - $this->assertInstanceOf('Gloudemans\Shoppingcart\CartCollection', $this->cart->content()); - } + $item = $this->getBuyableMock(); - public function testCartCollectionHasCartRowCollection() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); + $cartItem = $cart->add($item); - $this->cart->add('293ad', 'Product 1', 1, 9.99); + $this->assertInstanceOf(\Gloudemans\Shoppingcart\CartItem::class, $cartItem); + $this->assertEquals('027c91341fd5cf4d2579b49c4b6a90da', $cartItem->rowId); + } - $this->assertInstanceOf('Gloudemans\Shoppingcart\CartRowCollection', $this->cart->content()->first()); - } + /** @test */ + public function it_can_add_multiple_buyable_items_at_once() + { + $this->expectsEvents('cart.added'); - public function testCartRowCollectionHasCartRowOptionsCollection() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); + $cart = $this->getCart(); - $this->cart->add('293ad', 'Product 1', 1, 9.99); + $item1 = $this->getBuyableMock(); + $item2 = $this->getBuyableMock(2); - $this->assertInstanceOf('Gloudemans\Shoppingcart\CartRowOptionsCollection', $this->cart->content()->first()->options); - } + $cart->add([$item1, $item2]); - public function testCartCanAssociateWithModel() - { - $this->cart->associate('TestProduct'); + $this->assertEquals(2, $cart->count()); + } - $this->assertEquals('TestProduct', PHPUnit_Framework_Assert::readAttribute($this->cart, 'associatedModel')); - } + /** @test */ + public function it_will_return_an_array_of_cartitems_when_you_add_multiple_items_at_once() + { + $this->expectsEvents('cart.added'); - public function testCartCanAssociateWithNamespacedModel() - { - $this->cart->associate('TestProduct', 'Acme\Test\Models'); + $cart = $this->getCart(); - $this->assertEquals('TestProduct', PHPUnit_Framework_Assert::readAttribute($this->cart, 'associatedModel')); - $this->assertEquals('Acme\Test\Models', PHPUnit_Framework_Assert::readAttribute($this->cart, 'associatedModelNamespace')); - } + $item1 = $this->getBuyableMock(); + $item2 = $this->getBuyableMock(2); - public function testCartCanReturnModelProperties() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); + $cartItems = $cart->add([$item1, $item2]); - $this->cart->associate('TestProduct')->add('293ad', 'Product 1', 1, 9.99); + $this->assertTrue(is_array($cartItems)); + $this->assertCount(2, $cartItems); + $this->assertContainsOnlyInstancesOf(\Gloudemans\Shoppingcart\CartItem::class, $cartItems); + } - $this->assertEquals('This is the description of the test model', $this->cart->get('8cbf215baa3b757e910e5305ab981172')->testproduct->description); - } + /** @test */ + public function it_can_add_an_item_from_attributes() + { + $this->expectsEvents('cart.added'); - public function testCartCanReturnNamespadedModelProperties() - { - $this->events->shouldReceive('fire')->once()->with('cart.add', m::type('array')); - $this->events->shouldReceive('fire')->once()->with('cart.added', m::type('array')); + $cart = $this->getCart(); - $this->cart->associate('TestProduct', 'Acme\Test\Models')->add('293ad', 'Product 1', 1, 9.99); + $cart->add(1, 'Test item', 1, 10.00); - $this->assertEquals('This is the description of the namespaced test model', $this->cart->get('8cbf215baa3b757e910e5305ab981172')->testproduct->description); - } + $this->assertEquals(1, $cart->count()); + } - /** - * @expectedException Gloudemans\Shoppingcart\Exceptions\ShoppingcartUnknownModelException - */ - public function testCartThrowsExceptionOnUnknownModel() - { - $this->cart->associate('NoneExistingModel'); - } + /** @test */ + public function it_can_add_an_item_from_an_array() + { + $this->expectsEvents('cart.added'); + $cart = $this->getCart(); + + $cart->add(['id' => 1, 'name' => 'Test item', 'qty' => 1, 'price' => 10.00]); + + $this->assertEquals(1, $cart->count()); + } + + /** @test */ + public function it_can_add_multiple_array_items_at_once() + { + $this->expectsEvents('cart.added'); + + $cart = $this->getCart(); + + $cart->add([ + ['id' => 1, 'name' => 'Test item 1', 'qty' => 1, 'price' => 10.00], + ['id' => 2, 'name' => 'Test item 2', 'qty' => 1, 'price' => 10.00] + ]); + + $this->assertEquals(2, $cart->count()); + } + + /** @test */ + public function it_can_add_an_item_with_options() + { + $this->expectsEvents('cart.added'); + + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $options = ['size' => 'XL', 'color' => 'red']; + + $cart->add($item, 1, $options); + + $cartItem = $cart->get('07d5da5550494c62daf9993cf954303f'); + + $this->assertInstanceOf(\Gloudemans\Shoppingcart\CartItem::class, $cartItem); + $this->assertEquals('XL', $cartItem->options->size); + $this->assertEquals('red', $cartItem->options->color); + } + + /** + * @test + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Please supply a valid identifier. + */ + public function it_will_validate_the_identifier() + { + $cart = $this->getCart(); + + $cart->add(null, 'Some title', 1, 10.00); + } + + /** + * @test + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Please supply a valid name. + */ + public function it_will_validate_the_name() + { + $cart = $this->getCart(); + + $cart->add(1, null, 1, 10.00); + } + + /** + * @test + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Please supply a valid quantity. + */ + public function it_will_validate_the_quantity() + { + $cart = $this->getCart(); + + $cart->add(1, 'Some title', 'invalid', 10.00); + } + + /** + * @test + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Please supply a valid price. + */ + public function it_will_validate_the_price() + { + $cart = $this->getCart(); + + $cart->add(1, 'Some title', 1, 'invalid'); + } + + /** @test */ + public function it_will_update_the_cart_if_the_item_already_exists_in_the_cart() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + $cart->add($item); + + $this->assertItemsInCart(2, $cart); + $this->assertRowsInCart(1, $cart); + } + + /** @test */ + public function it_can_update_the_quantity_of_an_existing_item_in_the_cart() + { + $this->expectsEvents('cart.updated'); + + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $cart->update('027c91341fd5cf4d2579b49c4b6a90da', 2); + + $this->assertItemsInCart(2, $cart); + $this->assertRowsInCart(1, $cart); + } + + /** @test */ + public function it_can_update_an_existing_item_in_the_cart_from_a_buyable() + { + $this->expectsEvents('cart.updated'); + + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $item2 = $this->getBuyableMock(1, 'Different description'); + + $cart->update('027c91341fd5cf4d2579b49c4b6a90da', $item2); + + $this->assertItemsInCart(1, $cart); + $this->assertEquals('Different description', $cart->get('027c91341fd5cf4d2579b49c4b6a90da')->name); + } + + /** @test */ + public function it_can_update_an_existing_item_in_the_cart_from_an_array() + { + $this->expectsEvents('cart.updated'); + + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $cart->update('027c91341fd5cf4d2579b49c4b6a90da', ['name' => 'Different description']); + + $this->assertItemsInCart(1, $cart); + $this->assertEquals('Different description', $cart->get('027c91341fd5cf4d2579b49c4b6a90da')->name); + } + + /** + * @test + * @expectedException \Gloudemans\Shoppingcart\Exceptions\InvalidRowIDException + */ + public function it_will_throw_an_exception_if_a_rowid_was_not_found() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $item2 = $this->getBuyableMock(1, 'Different description'); + + $cart->update('none-existing-rowid', $item2); + } + + /** @test */ + public function it_will_regenerate_the_rowid_if_the_options_changed() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item, 1, ['color' => 'red']); + + $cart->update('ea65e0bdcd1967c4b3149e9e780177c0', ['options' => ['color' => 'blue']]); + + $this->assertItemsInCart(1, $cart); + $this->assertEquals('7e70a1e9aaadd18c72921a07aae5d011', $cart->content()->first()->rowId); + $this->assertEquals('blue', $cart->get('7e70a1e9aaadd18c72921a07aae5d011')->options->color); + } + + /** @test */ + public function it_will_add_the_item_to_an_existing_row_if_the_options_changed_to_an_existing_rowid() + { + $cart = $this->getCart(); + + $item1 = $this->getBuyableMock(); + $item2 = $this->getBuyableMock(); + + $cart->add($item1, 1, ['color' => 'red']); + $cart->add($item2, 1, ['color' => 'blue']); + + $cart->update('7e70a1e9aaadd18c72921a07aae5d011', ['options' => ['color' => 'red']]); + + $this->assertItemsInCart(2, $cart); + $this->assertRowsInCart(1, $cart); + } + + /** @test */ + public function it_can_remove_an_item_from_the_cart() + { + $this->expectsEvents('cart.removed'); + + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $cart->remove('027c91341fd5cf4d2579b49c4b6a90da'); + + $this->assertItemsInCart(0, $cart); + $this->assertRowsInCart(0, $cart); + } + + /** @test */ + public function it_will_remove_the_item_if_its_quantity_was_set_to_zero() + { + $this->expectsEvents('cart.removed'); + + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $cart->update('027c91341fd5cf4d2579b49c4b6a90da', 0); + + $this->assertItemsInCart(0, $cart); + $this->assertRowsInCart(0, $cart); + } + + /** @test */ + public function it_will_remove_the_item_if_its_quantity_was_set_negative() + { + $this->expectsEvents('cart.removed'); + + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $cart->update('027c91341fd5cf4d2579b49c4b6a90da', -1); + + $this->assertItemsInCart(0, $cart); + $this->assertRowsInCart(0, $cart); + } + + /** @test */ + public function it_can_get_an_item_from_the_cart_by_its_rowid() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); + + $this->assertInstanceOf(\Gloudemans\Shoppingcart\CartItem::class, $cartItem); + } + + /** @test */ + public function it_can_get_the_content_of_the_cart() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + $item2 = $this->getBuyableMock(2); + + $cart->add($item); + $cart->add($item2); + + $content = $cart->content(); + + $this->assertInstanceOf(\Illuminate\Support\Collection::class, $content); + $this->assertCount(2, $content); + } + + /** @test */ + public function it_can_destroy_a_cart() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $this->assertItemsInCart(1, $cart); + + $cart->destroy(); + + $this->assertItemsInCart(0, $cart); + } + + /** @test */ + public function it_can_get_the_total_price_of_the_cart_content() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'First item', 10.00); + $item2 = $this->getBuyableMock(2, 'Second item', 25.00); + + $cart->add($item); + $cart->add($item2, 2); + + $this->assertItemsInCart(3, $cart); + $this->assertEquals(60.00, $cart->total); + } + + /** @test */ + public function it_can_return_a_formatted_total() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'First item', 1000.00); + $item2 = $this->getBuyableMock(2, 'Second item', 2500.00); + + $cart->add($item); + $cart->add($item2, 2); + + $this->assertItemsInCart(3, $cart); + $this->assertEquals('6.000,00', $cart->total(2, ',', '.')); + } + + /** @test */ + public function it_can_search_the_cart_for_a_specific_item() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some item'); + $item2 = $this->getBuyableMock(2, 'Another item'); + + $cart->add($item); + $cart->add($item2); + + $cartItem = $cart->search(function ($cartItem, $rowId) { + return $cartItem->name == 'Some item'; + }); + + $this->assertInstanceOf(\Gloudemans\Shoppingcart\CartItem::class, $cartItem); + $this->assertEquals(1, $cartItem->id); + } + + /** @test */ + public function it_can_search_the_cart_for_multiple_items() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some item'); + $item2 = $this->getBuyableMock(2, 'Some item'); + $item3 = $this->getBuyableMock(3, 'Another item'); + + $cart->add($item); + $cart->add($item2); + $cart->add($item3); + + $cartItem = $cart->search(function ($cartItem, $rowId) { + return $cartItem->name == 'Some item'; + }); + + $this->assertInstanceOf(\Illuminate\Support\Collection::class, $cartItem); + } + + /** @test */ + public function it_can_search_the_cart_for_a_specific_item_with_options() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some item'); + $item2 = $this->getBuyableMock(2, 'Another item'); + + $cart->add($item, 1, ['color' => 'red']); + $cart->add($item2, 1, ['color' => 'blue']); + + $cartItem = $cart->search(function ($cartItem, $rowId) { + return $cartItem->options->color == 'red'; + }); + + $this->assertInstanceOf(\Gloudemans\Shoppingcart\CartItem::class, $cartItem); + $this->assertEquals(1, $cartItem->id); + } + + /** @test */ + public function it_will_associate_the_cart_item_with_a_model_when_you_add_a_buyable() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); + + $this->assertEquals('Mockery_0_Gloudemans_Shoppingcart_Contracts_Buyable', PHPUnit_Framework_Assert::readAttribute($cartItem, 'associatedModel')); + } + + /** @test */ + public function it_can_associate_the_cart_item_with_a_model() + { + $cart = $this->getCart(); + + $model = Mockery::mock('MockModel'); + + $cart->add(1, 'Test item', 1, 10.00); + + $cart->associate('027c91341fd5cf4d2579b49c4b6a90da', $model); + + $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); + + $this->assertEquals(get_class($model), PHPUnit_Framework_Assert::readAttribute($cartItem, 'associatedModel')); + } + + /** + * @test + * @expectedException \Gloudemans\Shoppingcart\Exceptions\UnknownModelException + * @expectedExceptionMessage The supplied model SomeModel does not exist. + */ + public function it_will_throw_an_exception_when_a_non_existing_model_is_being_associated() + { + $cart = $this->getCart(); + + $cart->add(1, 'Test item', 1, 10.00); + + $cart->associate('027c91341fd5cf4d2579b49c4b6a90da', 'SomeModel'); + } + + /** @test */ + public function it_can_get_the_associated_model_of_a_cart_item() + { + $cart = $this->getCart(); + + $model = new ModelStub; + + $cart->add(1, 'Test item', 1, 10.00); + + $cart->associate('027c91341fd5cf4d2579b49c4b6a90da', $model); + + $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); + + $this->assertInstanceOf(ModelStub::class, $cartItem->model); + $this->assertEquals('Some value', $cartItem->model->someValue); + } + + /** @test */ + public function it_can_calculate_the_subtotal_of_a_cart_item() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some title', 9.99); + + $cart->add($item, 3); + + $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); + + $this->assertEquals(29.97, $cartItem->subtotal); + } + + /** @test */ + public function it_can_return_a_formatted_subtotal() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some title', 500); + + $cart->add($item, 3); + + $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); + + $this->assertEquals('1.500,00', $cartItem->subtotal(2, ',', '.')); + } + + /** @test */ + public function it_can_calculate_tax_based_on_the_default_tax_rate_in_the_config() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some title', 10.00); + + $cart->add($item, 1); + + $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); + + $this->assertEquals(2.10, $cartItem->tax); + } + + /** @test */ + public function it_can_calculate_tax_based_on_the_specified_tax() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some title', 10.00); + + $cart->add($item, 1); + + $cart->setTax('027c91341fd5cf4d2579b49c4b6a90da', 19); + + $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); + + $this->assertEquals(1.90, $cartItem->tax); + } + + /** @test */ + public function it_can_return_the_calculated_tax_formatted() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some title', 10000.00); + + $cart->add($item, 1); + + $cartItem = $cart->get('027c91341fd5cf4d2579b49c4b6a90da'); + + $this->assertEquals('2.100,00', $cartItem->tax(2, ',', '.')); + } + + /** @test */ + public function it_can_calculate_the_total_tax_for_all_cart_items() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some title', 10.00); + $item2 = $this->getBuyableMock(2, 'Some title', 20.00); + + $cart->add($item, 1); + $cart->add($item2, 2); + + $this->assertEquals(10.50, $cart->tax); + } + + /** @test */ + public function it_can_return_formatted_total_tax() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some title', 1000.00); + $item2 = $this->getBuyableMock(2, 'Some title', 2000.00); + + $cart->add($item, 1); + $cart->add($item2, 2); + + $this->assertEquals('1.050,00', $cart->tax(2, ',', '.')); + } + + /** @test */ + public function it_can_return_the_subtotal() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some title', 10.00); + $item2 = $this->getBuyableMock(2, 'Some title', 20.00); + + $cart->add($item, 1); + $cart->add($item2, 2); + + $this->assertEquals(39.50, $cart->subtotal); + } + + /** @test */ + public function it_can_return_formatted_subtotal() + { + $cart = $this->getCart(); + + $item = $this->getBuyableMock(1, 'Some title', 1000.00); + $item2 = $this->getBuyableMock(2, 'Some title', 2000.00); + + $cart->add($item, 1); + $cart->add($item2, 2); + + $this->assertEquals('3.950,00', $cart->subtotal(2, ',', '.')); + } + + /** @test */ + public function it_can_store_the_cart_in_a_database() + { + $this->artisan('migrate', [ + '--database' => 'testing', + '--realpath' => realpath(__DIR__.'/../database/migrations'), + ]); + + $this->expectsEvents('cart.stored'); + + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $cart->store($identifier = 123); + + $serialized = serialize($cart->content()); + + $this->seeInDatabase('shoppingcart', ['identifier' => $identifier, 'instance' => 'default', 'content' => $serialized]); + } + + /** + * @test + * @expectedException \Gloudemans\Shoppingcart\Exceptions\CartAlreadyStoredException + * @expectedExceptionMessage A cart with identifier 123 was already stored. + */ + public function it_will_throw_an_exception_when_a_cart_was_already_stored_using_the_specified_identifier() + { + $this->artisan('migrate', [ + '--database' => 'testing', + '--realpath' => realpath(__DIR__.'/../database/migrations'), + ]); + + $this->expectsEvents('cart.stored'); + + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $cart->store($identifier = 123); + + $cart->store($identifier); + } + + /** @test */ + public function it_can_restore_a_cart_from_the_database() + { + $this->artisan('migrate', [ + '--database' => 'testing', + '--realpath' => realpath(__DIR__.'/../database/migrations'), + ]); + + $this->expectsEvents('cart.restored'); + + $cart = $this->getCart(); + + $item = $this->getBuyableMock(); + + $cart->add($item); + + $cart->store($identifier = 123); + + $cart->destroy(); + + $this->assertItemsInCart(0, $cart); + + $cart->restore($identifier); + + $this->assertItemsInCart(1, $cart); + + $this->dontSeeInDatabase('shoppingcart', ['identifier' => $identifier, 'instance' => 'default']); + } + + /** @test */ + public function it_will_just_keep_the_current_instance_if_no_cart_with_the_given_identifier_was_stored() + { + $this->artisan('migrate', [ + '--database' => 'testing', + '--realpath' => realpath(__DIR__.'/../database/migrations'), + ]); + + $cart = $this->getCart(); + + $cart->restore($identifier = 123); + + $this->assertItemsInCart(0, $cart); + } + + /** + * Get an instance of the cart. + * + * @return \Gloudemans\Shoppingcart\Cart + */ + private function getCart() + { + $session = $this->app->make('session'); + $events = $this->app->make('events'); + + $cart = new Cart($session, $events); + + return $cart; + } + + /** + * Get a mock of a Buyable item. + * + * @param int $id + * @param string $name + * @param float $price + * @return \Mockery\MockInterface + */ + private function getBuyableMock($id = 1, $name = 'Item name', $price = 10.00) + { + $item = Mockery::mock(Buyable::class)->shouldIgnoreMissing(); + + $item->shouldReceive('getBuyableIdentifier')->andReturn($id); + $item->shouldReceive('getBuyableDescription')->andReturn($name); + $item->shouldReceive('getBuyablePrice')->andReturn($price); + + return $item; + } } +class ModelStub { + public $someValue = 'Some value'; + public function find($id) { return $this; } +} \ No newline at end of file diff --git a/tests/helpers/NamespacedProductModelStub.php b/tests/helpers/NamespacedProductModelStub.php deleted file mode 100644 index 887c108..0000000 --- a/tests/helpers/NamespacedProductModelStub.php +++ /dev/null @@ -1,5 +0,0 @@ -session[$key]); - } - public function get($key) - { - return $this->session[$key]; - } - public function put($key, $value) - { - $this->session[$key] = $value; - } -} \ No newline at end of file