├── .gitignore ├── Cart.php ├── CartItem.php ├── LICENSE ├── README.md ├── calculators ├── CalculatorInterface.php └── SimpleCalculator.php ├── composer.json ├── docs └── guide-ru.md ├── migrations └── m180604_202836_create_cart_items_table.php ├── phpunit.xml.dist ├── storage ├── CookieStorage.php ├── DbSessionStorage.php ├── SessionStorage.php └── StorageInterface.php └── tests ├── CartTest.php ├── CookieStorageTest.php ├── SessionStorageTest.php ├── SimpleCalculatorTest.php ├── TestCase.php ├── bootstrap.php └── data ├── DummyProduct.php └── DummyStorage.php /.gitignore: -------------------------------------------------------------------------------- 1 | # cache directories 2 | Thumbs.db 3 | *.DS_Store 4 | *.empty 5 | 6 | #phpstorm project files 7 | .idea 8 | 9 | #netbeans project files 10 | nbproject 11 | 12 | #eclipse, zend studio, aptana or other eclipse like project files 13 | .buildpath 14 | .project 15 | .settings 16 | 17 | # mac deployment helpers 18 | switch 19 | index 20 | 21 | vendor/ 22 | composer.lock 23 | -------------------------------------------------------------------------------- /Cart.php: -------------------------------------------------------------------------------- 1 | 'cart', 30 | 'expire' => 604800, 31 | 'productClass' => 'app\model\Product', 32 | 'productFieldId' => 'id', 33 | 'productFieldPrice' => 'price', 34 | ]; 35 | 36 | /** 37 | * @var CartItem[] 38 | */ 39 | private $items; 40 | 41 | /** 42 | * @var \devanych\cart\storage\StorageInterface 43 | */ 44 | private $storage; 45 | 46 | /** 47 | * @var \devanych\cart\calculators\CalculatorInterface 48 | */ 49 | private $calculator; 50 | 51 | /** 52 | * @inheritdoc 53 | */ 54 | public function init() 55 | { 56 | parent::init(); 57 | 58 | $this->params = array_merge($this->defaultParams, $this->params); 59 | 60 | if (!class_exists($this->params['productClass'])) { 61 | throw new InvalidConfigException('productClass `' . $this->params['productClass'] . '` not found'); 62 | } 63 | if (!class_exists($this->storageClass)) { 64 | throw new InvalidConfigException('storageClass `' . $this->storageClass . '` not found'); 65 | } 66 | if (!class_exists($this->calculatorClass)) { 67 | throw new InvalidConfigException('calculatorClass `' . $this->calculatorClass . '` not found'); 68 | } 69 | 70 | $this->storage = new $this->storageClass($this->params); 71 | $this->calculator = new $this->calculatorClass(); 72 | } 73 | 74 | /** 75 | * Add an item to the cart 76 | * @param object $product 77 | * @param integer $quantity 78 | * @return void 79 | */ 80 | public function add($product, $quantity) 81 | { 82 | $this->loadItems(); 83 | if (isset($this->items[$product->{$this->params['productFieldId']}])) { 84 | $this->plus($product->{$this->params['productFieldId']}, $quantity); 85 | } else { 86 | $this->items[$product->{$this->params['productFieldId']}] = new CartItem($product, $quantity, $this->params); 87 | ksort($this->items, SORT_NUMERIC); 88 | $this->saveItems(); 89 | } 90 | } 91 | 92 | /** 93 | * Adding item quantity in the cart 94 | * @param integer $id 95 | * @param integer $quantity 96 | * @return void 97 | */ 98 | public function plus($id, $quantity) 99 | { 100 | $this->loadItems(); 101 | if (isset($this->items[$id])) { 102 | $this->items[$id]->setQuantity($quantity + $this->items[$id]->getQuantity()); 103 | } 104 | $this->saveItems(); 105 | } 106 | 107 | /** 108 | * Change item quantity in the cart 109 | * @param integer $id 110 | * @param integer $quantity 111 | * @return void 112 | */ 113 | public function change($id, $quantity) 114 | { 115 | $this->loadItems(); 116 | if (isset($this->items[$id])) { 117 | $this->items[$id]->setQuantity($quantity); 118 | } 119 | $this->saveItems(); 120 | } 121 | 122 | /** 123 | * Removes an items from the cart 124 | * @param integer $id 125 | * @return void 126 | */ 127 | public function remove($id) 128 | { 129 | $this->loadItems(); 130 | if (array_key_exists($id, $this->items)) { 131 | unset($this->items[$id]); 132 | } 133 | $this->saveItems(); 134 | } 135 | 136 | /** 137 | * Removes all items from the cart 138 | * @return void 139 | */ 140 | public function clear() 141 | { 142 | $this->items = []; 143 | $this->saveItems(); 144 | } 145 | 146 | /** 147 | * Returns all items from the cart 148 | * @return CartItem[] 149 | */ 150 | public function getItems() 151 | { 152 | $this->loadItems(); 153 | return $this->items; 154 | } 155 | 156 | /** 157 | * Returns an item from the cart 158 | * @param integer $id 159 | * @return CartItem 160 | */ 161 | public function getItem($id) 162 | { 163 | $this->loadItems(); 164 | return isset($this->items[$id]) ? $this->items[$id] : null; 165 | } 166 | 167 | /** 168 | * Returns ids array all items from the cart 169 | * @return array 170 | */ 171 | public function getItemIds() 172 | { 173 | $this->loadItems(); 174 | $items = []; 175 | foreach ($this->items as $item) { 176 | $items[] = $item->getId(); 177 | } 178 | return $items; 179 | } 180 | 181 | /** 182 | * Returns total cost all items from the cart 183 | * @return integer 184 | */ 185 | public function getTotalCost() 186 | { 187 | $this->loadItems(); 188 | return $this->calculator->getCost($this->items); 189 | } 190 | 191 | /** 192 | * Returns total count all items from the cart 193 | * @return integer 194 | */ 195 | public function getTotalCount() 196 | { 197 | $this->loadItems(); 198 | return $this->calculator->getCount($this->items); 199 | } 200 | 201 | /** 202 | * Load all items from the cart 203 | * @return void 204 | */ 205 | private function loadItems() 206 | { 207 | if ($this->items === null) { 208 | $this->items = $this->storage->load(); 209 | } 210 | } 211 | 212 | /** 213 | * Save all items to the cart 214 | * @return void 215 | */ 216 | private function saveItems() 217 | { 218 | $this->storage->save($this->items); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /CartItem.php: -------------------------------------------------------------------------------- 1 | product = $product; 23 | $this->quantity = $quantity; 24 | $this->params = $params; 25 | } 26 | 27 | /** 28 | * Returns the id of the item 29 | * @return integer 30 | */ 31 | public function getId() 32 | { 33 | return $this->product->{$this->params['productFieldId']}; 34 | } 35 | 36 | /** 37 | * Returns the price of the item 38 | * @return integer|float 39 | */ 40 | public function getPrice() 41 | { 42 | return $this->product->{$this->params['productFieldPrice']}; 43 | } 44 | 45 | /** 46 | * Returns the product, AR model 47 | * @return object 48 | */ 49 | public function getProduct() 50 | { 51 | return $this->product; 52 | } 53 | 54 | /** 55 | * Returns the cost of the item 56 | * @return integer|float 57 | */ 58 | public function getCost() 59 | { 60 | return ceil($this->getPrice() * $this->quantity); 61 | } 62 | 63 | /** 64 | * Returns the quantity of the item 65 | * @return integer 66 | */ 67 | public function getQuantity() 68 | { 69 | return $this->quantity; 70 | } 71 | 72 | /** 73 | * Sets the quantity of the item 74 | * @param integer $quantity 75 | * @return void 76 | */ 77 | public function setQuantity($quantity) 78 | { 79 | $this->quantity = $quantity; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, Evgeniy Zyubin (Devanych) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of sitemap nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yii2 shopping cart 2 | 3 | This extension adds shopping cart for Yii framework 2.0 4 | 5 | Guide with a detailed description in Russian language [here](https://github.com/devanych/yii2-cart/blob/master/docs/guide-ru.md). 6 | 7 | ## Installation 8 | 9 | The preferred way to install this extension is through [Composer](https://getcomposer.org/download/) 10 | 11 | Either run 12 | 13 | ``` 14 | php composer.phar require devanych/yii2-cart "*" 15 | ``` 16 | 17 | or add 18 | 19 | ``` 20 | devanych/yii2-cart: "*" 21 | ``` 22 | 23 | to the `require` section of your `composer.json` file. 24 | 25 | ## Configuration 26 | 27 | Configure the `cart` component (default values are shown): 28 | 29 | ```php 30 | return [ 31 | //... 32 | 'components' => [ 33 | //... 34 | 'cart' => [ 35 | 'class' => 'devanych\cart\Cart', 36 | 'storageClass' => 'devanych\cart\storage\SessionStorage', 37 | 'calculatorClass' => 'devanych\cart\calculators\SimpleCalculator', 38 | 'params' => [ 39 | 'key' => 'cart', 40 | 'expire' => 604800, 41 | 'productClass' => 'app\model\Product', 42 | 'productFieldId' => 'id', 43 | 'productFieldPrice' => 'price', 44 | ], 45 | ], 46 | ] 47 | //... 48 | ]; 49 | ``` 50 | 51 | In addition to `devanych\cart\storage\SessionStorage`, there is also `devanych\cart\storage\CookieStorage` and `devanych\cart\storage\DbSessionStorage`. It is possible to create your own storage, you need to implement the interface `devanych\cart\storage\StorageInterface`. 52 | 53 | `DbSessionStorage` uses `SessionStorage` for unauthorized users and database for authorized. 54 | 55 | > If you use the `devanych\cart\storage\DbSessionStorage` as `storageClass` then you need to apply the following migration: 56 | 57 | ```php 58 | php yii migrate --migrationPath=@vendor/devanych/yii2-cart/migrations 59 | ``` 60 | 61 | `devanych\cart\calculators\SimpleCalculator` produces the usual calculation of the total cost and total quantity of items in the cart. If you need to make a calculation with discounts or something else, you can create your own calculator by implementing the interface `devanych\cart\calculators\CalculatorInterface`. 62 | 63 | Setting up the `params` array: 64 | 65 | * `key` - For Session and Cookie. 66 | 67 | * `expire` - Cookie life time. 68 | 69 | * `productClass` - Product class is an ActiveRecord model. 70 | 71 | * `productFieldId` - Name of the product model `id` field. 72 | 73 | * `productFieldPrice` - Name of the product model `price` field. 74 | 75 | #### Supporting multiple shopping carts to same website: 76 | 77 | ```php 78 | //... 79 | 'cart' => [ 80 | 'class' => 'devanych\cart\Cart', 81 | 'storageClass' => 'devanych\cart\storage\SessionStorage', 82 | 'calculatorClass' => 'devanych\cart\calculators\SimpleCalculator', 83 | 'params' => [ 84 | 'key' => 'cart', 85 | 'expire' => 604800, 86 | 'productClass' => 'app\model\Product', 87 | 'productFieldId' => 'id', 88 | 'productFieldPrice' => 'price', 89 | ], 90 | ], 91 | 'favorite' => [ 92 | 'class' => 'devanych\cart\Cart', 93 | 'storageClass' => 'devanych\cart\storage\DbSessionStorage', 94 | 'calculatorClass' => 'devanych\cart\calculators\SimpleCalculator', 95 | 'params' => [ 96 | 'key' => 'favorite', 97 | 'expire' => 604800, 98 | 'productClass' => 'app\models\Product', 99 | 'productFieldId' => 'id', 100 | 'productFieldPrice' => 'price', 101 | ], 102 | ], 103 | //... 104 | ``` 105 | 106 | ## Usage 107 | 108 | You can get the shopping cart component anywhere in the app using `Yii::$app->cart`. 109 | 110 | Using cart: 111 | 112 | ```php 113 | // Product is an AR model 114 | $product = Product::findOne(1); 115 | 116 | // Get component of the cart 117 | $cart = \Yii::$app->cart; 118 | 119 | // Add an item to the cart 120 | $cart->add($product, $quantity); 121 | 122 | // Adding item quantity in the cart 123 | $cart->plus($product->id, $quantity); 124 | 125 | // Change item quantity in the cart 126 | $cart->change($product->id, $quantity); 127 | 128 | // Removes an items from the cart 129 | $cart->remove($product->id); 130 | 131 | // Removes all items from the cart 132 | $cart->clear(); 133 | 134 | // Get all items from the cart 135 | $cart->getItems(); 136 | 137 | // Get an item from the cart 138 | $cart->getItem($product->id); 139 | 140 | // Get ids array all items from the cart 141 | $cart->getItemIds(); 142 | 143 | // Get total cost all items from the cart 144 | $cart->getTotalCost(); 145 | 146 | // Get total count all items from the cart 147 | $cart->getTotalCount(); 148 | ``` 149 | 150 | #### Using cart items: 151 | 152 | ```php 153 | // Product is an AR model 154 | $product = Product::findOne(1); 155 | 156 | // Get component of the cart 157 | $cart = \Yii::$app->cart; 158 | 159 | // Get an item from the cart 160 | $item = $cart->getItem($product->id); 161 | 162 | // Get the id of the item 163 | $item->getId(); 164 | 165 | // Get the price of the item 166 | $item->getPrice(); 167 | 168 | // Get the product, AR model 169 | $item->getProduct(); 170 | 171 | // Get the cost of the item 172 | $item->getCost(); 173 | 174 | // Get the quantity of the item 175 | $item->getQuantity(); 176 | 177 | // Set the quantity of the item 178 | $item->setQuantity($quantity); 179 | ``` 180 | 181 | > By using method `getProduct()`, you have access to all the properties and methods of the product. 182 | 183 | ```php 184 | $product = $item->getProduct(); 185 | 186 | echo $product->name; 187 | ``` 188 | -------------------------------------------------------------------------------- /calculators/CalculatorInterface.php: -------------------------------------------------------------------------------- 1 | getCost(); 16 | } 17 | return $cost; 18 | } 19 | 20 | /** 21 | * @param \devanych\cart\CartItem[] $items 22 | * @return integer 23 | */ 24 | public function getCount(array $items) 25 | { 26 | $count = 0; 27 | foreach ($items as $item) { 28 | $count += $item->getQuantity(); 29 | } 30 | return $count; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "devanych/yii2-cart", 3 | "description": "Shopping cart for Yii2", 4 | "keywords": ["yii2-cart", "yii2-shopping-cart", "yii2-extension", "yii2"], 5 | "type": "yii2-extension", 6 | "license": "BSD-3-Clause", 7 | "authors": [ 8 | { 9 | "name": "Evgeniy Zyubin", 10 | "email": "info@zyubin.ru", 11 | "homepage": "https://zyubin.ru" 12 | } 13 | ], 14 | "support": { 15 | "issues": "https://github.com/devanych/yii2-cart/issues?state=open", 16 | "source": "https://github.com/devanych/yii2-cart" 17 | }, 18 | "require": { 19 | "php": ">=5.6", 20 | "yiisoft/yii2": "*" 21 | }, 22 | "require-dev": { 23 | "phpunit/phpunit": "~6.0" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "devanych\\cart\\": "" 28 | } 29 | }, 30 | "repositories": [ 31 | { 32 | "type": "composer", 33 | "url": "https://asset-packagist.org" 34 | } 35 | ] 36 | } -------------------------------------------------------------------------------- /docs/guide-ru.md: -------------------------------------------------------------------------------- 1 | # Расширение корзина для Yii2 2 | 3 | Корзина — это обязательный компонент для любого интернет-магазина, но ее функциональность в разных проектах может различаться. На одном сайте корзина работает с сессией, на втором — с кукисами, а на третьем — с базой данных, также не исключено, что со временем хранилище может меняться. 4 | 5 | Тоже касается и подсчета стоимости, например: нужно считать цену товара со скидкой в определенный день недели или в какой-то праздник, но это все невозможно предусмотреть при разработке корзины, поэтому должна быть возможность удобной кастомизации в будущем. 6 | 7 | Расширение «[devanych/yii2-cart](https://github.com/devanych/yii2-cart)» решает эти проблемы и позволяет очень легко менять хранилища и калькуляторы, дает возможность подключать собственные решения. 8 | 9 | Установить расширение через «[Composer](https://getcomposer.org/download/)»: 10 | 11 | ``` 12 | php composer.phar require devanych/yii2-cart "*" 13 | ``` 14 | 15 | или прописать зависимость в разделе `require` в файле `composer.json`: 16 | 17 | ``` 18 | devanych/yii2-cart: "*" 19 | ``` 20 | 21 | и выполнить в терминале `composer update`. 22 | 23 | ## Конфигурация 24 | 25 | В конфигурационном файле Yii2 приложения (`web.php` в «***basic***» и `main.php` в «***advanced***») в секцию `components` помещаем следующий код. 26 | 27 | ```php 28 | return [ 29 | //... 30 | 'components' => [ 31 | //... 32 | 'cart' => [ 33 | 'class' => 'devanych\cart\Cart', 34 | 'storageClass' => 'devanych\cart\storage\SessionStorage', 35 | 'calculatorClass' => 'devanych\cart\calculators\SimpleCalculator', 36 | 'params' => [ 37 | 'key' => 'cart', 38 | 'expire' => 604800, 39 | 'productClass' => 'app\model\Product', 40 | 'productFieldId' => 'id', 41 | 'productFieldPrice' => 'price', 42 | ], 43 | ], 44 | ] 45 | //... 46 | ]; 47 | ``` 48 | 49 | Свойство `storageClass` отвечает за хранилище, используемое корзиной, по умолчанию это `devanych\cart\storage\SessionStorage`. Сессионное хранилище можно поменять на `devanych\cart\storage\CookieStorage` или на `devanych\cart\storage\DbSessionStorage`. 50 | 51 | `DbSessionStorage` использует сессионное хранилище для не авторизованных пользователей и базу данных для авторизованных. Для его использования необходимо применить следующую миграцию. 52 | 53 | ``` 54 | php yii migrate --migrationPath=@vendor/devanych/yii2-cart/migrations 55 | ``` 56 | 57 | Если этих хранилищ окажется недостаточно, то вы можете создать собственное. Для этого необходимо реализовать интерфейс `devanych\cart\storage\StorageInterface` и указать свой созданный класс значением свойства `storageClass`. 58 | 59 | Свойству `calculatorClass` присвоено имя класса калькулятора `devanych\cart\calculators\SimpleCalculator`, этот класс подсчитывает общую стоимость и количество всех элементов корзины. Для реализации собственного калькулятора нужно реализовать интерфейс `devanych\cart\calculators\CalculatorInterface`. 60 | 61 | Разберем все дополнительные настройки, находящиеся в подмассиве `params`: 62 | 63 | * `key` — имя необходимое для сессии и куки (по умолчанию — `cart`); 64 | * `expire` — срок жизни cookie (по умолчанию — `604800`, т.е. неделя); 65 | * `productClass` — класс товара ActiveRecord модели (по умолчанию — `app\model\Product`); 66 | * `productFieldId` — первичный ключ модели товара (по умолчанию — `id`); 67 | * `productFieldId` — свойство (поле в БД) цены модели товара (по умолчанию — `price`). 68 | 69 | Вы можете использовать несколько компонентов корзины, например, хранить еще избранные товары: 70 | 71 | ```php 72 | //... 73 | 'cart' => [ 74 | 'class' => 'devanych\cart\Cart', 75 | 'storageClass' => 'devanych\cart\storage\SessionStorage', 76 | 'calculatorClass' => 'devanych\cart\calculators\SimpleCalculator', 77 | 'params' => [ 78 | 'key' => 'cart', 79 | 'expire' => 604800, 80 | 'productClass' => 'app\model\Product', 81 | 'productFieldId' => 'id', 82 | 'productFieldPrice' => 'price', 83 | ], 84 | ], 85 | 'favorite' => [ 86 | 'class' => 'devanych\cart\Cart', 87 | 'storageClass' => 'devanych\cart\storage\DbSessionStorage', 88 | 'calculatorClass' => 'devanych\cart\calculators\SimpleCalculator', 89 | 'params' => [ 90 | 'key' => 'favorite', 91 | 'expire' => 604800, 92 | 'productClass' => 'app\models\Product', 93 | 'productFieldId' => 'id', 94 | 'productFieldPrice' => 'price', 95 | ], 96 | ], 97 | //... 98 | ``` 99 | 100 | ## Использование 101 | 102 | Подключение корзины как компонента дает возможность обращения к ней практически из любого места приложения, используя сервис локатор `Yii::$app`. 103 | 104 | Использование корзины: 105 | 106 | ```php 107 | // Товар, объект AR модели 108 | $product = Product::findOne(1); 109 | 110 | // Компонент корзины 111 | $cart = \Yii::$app->cart; 112 | 113 | // Создает элемент корзины из переданного товара и его кол-ва 114 | $cart->add($product, $quantity); 115 | 116 | // Добавляет кол-во существующего элемента корзины 117 | $cart->plus($product->id, $quantity); 118 | 119 | // Изменяет кол-во существующего элемента корзины 120 | $cart->change($product->id, $quantity); 121 | 122 | // Удаляет конкретный элемент из корзины, объект `devanych\cart\CartItem` 123 | $cart->remove($product->id); 124 | 125 | // Удаляет все элемент из корзины 126 | $cart->clear(); 127 | 128 | // Получает все элемент из корзины 129 | $cart->getItems(); 130 | 131 | // Получает конкретный элемент из корзины 132 | $cart->getItem($product->id); 133 | 134 | // Получает идентификаторы всех элементов 135 | $cart->getItemIds(); 136 | 137 | // Получает общую стоимость всех элементов 138 | $cart->getTotalCost(); 139 | 140 | // Получает общее количество всех элементов 141 | $cart->getTotalCount(); 142 | ``` 143 | 144 | Использование элементов корзины: 145 | 146 | ```php 147 | // Товар, объект AR модели 148 | $product = Product::findOne(1); 149 | 150 | // Компонент корзины 151 | $cart = \Yii::$app->cart; 152 | 153 | // Получает конкретный элемент из корзины, объект `devanych\cart\CartItem` 154 | $item = $cart->getItem($product->id); 155 | 156 | // Получает идентификатор элемента равному идентификатор товара 157 | $item->getId(); 158 | 159 | // Получает цену элемента равную цене товара 160 | $item->getPrice(); 161 | 162 | // Получает товар, объект AR модели 163 | $item->getProduct(); 164 | 165 | // Получает общую стоимость товара хранящегося в элементе по его количеству 166 | $item->getCost(); 167 | 168 | // Получает общее кол-во товара хранящегося в элементе корзины 169 | $item->getQuantity(); 170 | 171 | // Устанавливает кол-во товара хранящегося в элементе корзины 172 | $item->setQuantity($quantity); 173 | ``` 174 | 175 | Стоит отметить, что метод `getProduct()` элемента корзины (`devanych\cart\CartItem`) возвращает полноценный объект ActiveRecord модели, это очень удобно, если нужно вывести какую-то информацию о хранящемся в корзине товаре. 176 | 177 | ## Простая реализация контроллера и представления 178 | 179 | Данное расширение дает возможность реализации любого представления для корзины. Я специально не стал делать дефолтный контроллер и представление, так как в каждом проекте корзина реализовывается по-разному: где-то просто, где-то без перезагрузки страницы, где-то в модальном окне, и т.д., а это значит, что и код будет отличаться. 180 | 181 | В самом примитивном варианте, если «запихать» все в контроллер (хотя так делать не нужно `;-))`, класс контроллера будет выглядеть так. 182 | 183 | ```php 184 | namespace app\controllers; 185 | 186 | use Yii; 187 | use yii\helpers\Html; 188 | use yii\web\Controller; 189 | use app\models\Product; 190 | 191 | class CartController extends Controller 192 | { 193 | /** 194 | * @var \devanych\cart\Cart $cart 195 | */ 196 | private $cart; 197 | 198 | public function __construct($id, $module, $config = []) 199 | { 200 | parent::__construct($id, $module, $config); 201 | $this->cart = Yii::$app->cart; 202 | } 203 | 204 | public function actionIndex() 205 | { 206 | return $this->render('index', [ 207 | 'cart' => $this->cart, 208 | ]); 209 | } 210 | 211 | public function actionAdd($id, $qty = 1) 212 | { 213 | try { 214 | $product = $this->getProduct($id); 215 | $quantity = $this->getQuantity($qty, $product->quantity); 216 | if ($item = $this->cart->getItem($product->id)) { 217 | $this->cart->plus($item->getId(), $quantity); 218 | } else { 219 | $this->cart->add($product, $quantity); 220 | } 221 | } catch (\DomainException $e) { 222 | Yii::$app->errorHandler->logException($e); 223 | Yii::$app->session->setFlash('error', $e->getMessage()); 224 | } 225 | return $this->redirect(['index']); 226 | } 227 | 228 | public function actionChange($id, $qty = 1) 229 | { 230 | try { 231 | $product = $this->getProduct($id); 232 | $quantity = $this->getQuantity($qty, $product->quantity); 233 | if ($item = $this->cart->getItem($product->id)) { 234 | $this->cart->change($item->getId(), $quantity); 235 | } 236 | } catch (\DomainException $e) { 237 | Yii::$app->errorHandler->logException($e); 238 | Yii::$app->session->setFlash('error', $e->getMessage()); 239 | } 240 | return $this->redirect(['index']); 241 | } 242 | 243 | public function actionRemove($id) 244 | { 245 | try { 246 | $product = $this->getProduct($id); 247 | $this->cart->remove($product->id); 248 | } catch (\DomainException $e) { 249 | Yii::$app->errorHandler->logException($e); 250 | Yii::$app->session->setFlash('error', $e->getMessage()); 251 | } 252 | return $this->redirect(['index']); 253 | } 254 | 255 | public function actionClear() 256 | { 257 | $this->cart->clear(); 258 | return $this->redirect(['index']); 259 | } 260 | 261 | /** 262 | * @param integer $id 263 | * @return Product the loaded model 264 | * @throws \DomainException if the product cannot be found 265 | */ 266 | private function getProduct($id) 267 | { 268 | if (($product = Product::findOne((int)$id)) !== null) { 269 | return $product; 270 | } 271 | throw new \DomainException('Товар не найден'); 272 | } 273 | 274 | /** 275 | * @param integer $qty 276 | * @param integer $maxQty 277 | * @return integer 278 | * @throws \DomainException if the product cannot be found 279 | */ 280 | private function getQuantity($qty, $maxQty) 281 | { 282 | $quantity = (int)$qty > 0 ? (int)$qty : 1; 283 | if ($quantity > $maxQty) { 284 | throw new \DomainException('Товара в наличии всего ' . Html::encode($maxQty) . ' шт.'); 285 | } 286 | return $quantity; 287 | } 288 | } 289 | ``` 290 | 291 | Ну и осталось создать представление `index.php`, в которое передается корзина, пройтись по ней в цикле и вывести информацию о добавленных товарах либо использовать `GridView`. 292 | 293 | ```html 294 | 301 | getItems())): ?> 302 |
303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 |
ФотоНаименованиеКол-воЦенаСумма
getProduct()->photo}", ['alt' => $item->getProduct()->name, 'width' => 50])?>getProduct()->name ?>getQuantity()?>getPrice()?>getCost()?>Удалить
Общее кол-во:getTotalCount()?>
Общая сумма:getTotalCost() ?>
335 |
336 | 337 |

Корзина пуста

338 | 339 | ``` 340 | -------------------------------------------------------------------------------- /migrations/m180604_202836_create_cart_items_table.php: -------------------------------------------------------------------------------- 1 | db->driverName === 'mysql' || $this->db->driverName === 'mariadb') { 13 | $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_general_ci ENGINE=InnoDB'; 14 | } 15 | 16 | $this->createTable('{{%cart_items}}', [ 17 | 'user_id' => $this->integer()->unsigned()->notNull(), 18 | 'product_id' => $this->integer()->unsigned()->notNull(), 19 | 'quantity' => $this->integer()->unsigned()->notNull(), 20 | ], $tableOptions); 21 | 22 | $this->addPrimaryKey('{{%pk-cart_items}}', '{{%cart_items}}', ['user_id', 'product_id']); 23 | 24 | $this->createIndex('{{%idx-cart_items-user_id}}', '{{%cart_items}}', 'user_id'); 25 | $this->createIndex('{{%idx-cart_items-product_id}}', '{{%cart_items}}', 'product_id'); 26 | } 27 | 28 | public function safeDown() 29 | { 30 | $this->dropTable('{{%cart_items}}'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | ./tests 11 | 12 | 13 | -------------------------------------------------------------------------------- /storage/CookieStorage.php: -------------------------------------------------------------------------------- 1 | params = $params; 20 | } 21 | 22 | /** 23 | * @return CartItem[] 24 | */ 25 | public function load() 26 | { 27 | if ($cookie = Yii::$app->request->cookies->get($this->params['key'])) { 28 | return array_filter(array_map(function (array $row) { 29 | if (isset($row['id'], $row['quantity']) && $product = $this->findProduct($row['id'])) { 30 | return new CartItem($product, $row['quantity'], $this->params); 31 | } 32 | return false; 33 | }, Json::decode($cookie->value))); 34 | } 35 | return []; 36 | } 37 | 38 | /** 39 | * @param CartItem[] $items 40 | * @return void 41 | */ 42 | public function save(array $items) 43 | { 44 | Yii::$app->response->cookies->add(new Cookie([ 45 | 'name' => $this->params['key'], 46 | 'value' => Json::encode(array_map(function (CartItem $item) { 47 | return [ 48 | 'id' => $item->getId(), 49 | 'quantity' => $item->getQuantity(), 50 | ]; 51 | }, $items)), 52 | 'expire' => time() + $this->params['expire'], 53 | ])); 54 | } 55 | 56 | /** 57 | * @param integer $productId 58 | * @return object|null 59 | */ 60 | private function findProduct($productId) 61 | { 62 | return $this->params['productClass']::find() 63 | ->where([$this->params['productFieldId'] => $productId]) 64 | ->limit(1) 65 | ->one(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /storage/DbSessionStorage.php: -------------------------------------------------------------------------------- 1 | params = $params; 40 | $this->db = Yii::$app->db; 41 | $this->userId = Yii::$app->user->id; 42 | $this->sessionStorage = new SessionStorage($this->params); 43 | } 44 | 45 | /** 46 | * @return CartItem[] 47 | */ 48 | public function load() 49 | { 50 | if (Yii::$app->user->isGuest) { 51 | return $this->sessionStorage->load(); 52 | } 53 | $this->moveItems(); 54 | return $this->loadDb(); 55 | } 56 | 57 | /** 58 | * @param CartItem[] $items 59 | * @return void 60 | */ 61 | public function save(array $items) 62 | { 63 | if (Yii::$app->user->isGuest) { 64 | $this->sessionStorage->save($items); 65 | } else { 66 | $this->moveItems(); 67 | $this->saveDb($items); 68 | } 69 | } 70 | 71 | /** 72 | * Moves all items from session storage to database storage 73 | * @return void 74 | */ 75 | private function moveItems() 76 | { 77 | if ($sessionItems = $this->sessionStorage->load()) { 78 | $items = ArrayHelper::index(ArrayHelper::merge($this->loadDb(), $sessionItems), function (CartItem $item) { 79 | return $item->getId(); 80 | }); 81 | $this->saveDb($items); 82 | $this->sessionStorage->save([]); 83 | } 84 | } 85 | 86 | /** 87 | * Load all items from the database 88 | * @return CartItem[] 89 | */ 90 | private function loadDb() 91 | { 92 | $rows = (new Query()) 93 | ->select('*') 94 | ->from($this->table) 95 | ->where(['user_id' => $this->userId]) 96 | ->all(); 97 | 98 | $items = []; 99 | foreach ($rows as $row) { 100 | $product = $this->params['productClass']::find() 101 | ->where([$this->params['productFieldId'] => $row['product_id']]) 102 | ->limit(1) 103 | ->one(); 104 | if ($product) { 105 | $items[$product->{$this->params['productFieldId']}] = new CartItem($product, $row['quantity'], $this->params); 106 | } 107 | } 108 | return $items; 109 | } 110 | 111 | /** 112 | * Save all items to the database 113 | * @param CartItem[] $items 114 | * @return void 115 | */ 116 | private function saveDb(array $items) 117 | { 118 | $this->db->createCommand()->delete($this->table, ['user_id' => $this->userId])->execute(); 119 | 120 | $this->db->createCommand()->batchInsert( 121 | $this->table, 122 | ['user_id', 'product_id', 'quantity'], 123 | array_map(function (CartItem $item) { 124 | return [ 125 | 'user_id' => $this->userId, 126 | 'product_id' => $item->getId(), 127 | 'quantity' => $item->getQuantity(), 128 | ]; 129 | }, $items) 130 | )->execute(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /storage/SessionStorage.php: -------------------------------------------------------------------------------- 1 | params = $params; 17 | } 18 | 19 | /** 20 | * @return \devanych\cart\models\CartItem[] 21 | */ 22 | public function load() 23 | { 24 | return Yii::$app->session->get($this->params['key'], []); 25 | } 26 | 27 | /** 28 | * @param \devanych\cart\models\CartItem[] $items 29 | * @return void 30 | */ 31 | public function save(array $items) 32 | { 33 | Yii::$app->session->set($this->params['key'], $items); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /storage/StorageInterface.php: -------------------------------------------------------------------------------- 1 | cart = \Yii::$app->cart; 24 | $this->product = new DummyProduct(); 25 | } 26 | 27 | public function testCreate() 28 | { 29 | $this->assertEquals([], \Yii::$app->cart->getItems()); 30 | } 31 | 32 | public function testAdd() 33 | { 34 | $this->cart->add($this->product, 3); 35 | $this->assertEquals(1, count($items = $this->cart->getItems())); 36 | $this->assertEquals(5, $items[5]->getId()); 37 | $this->assertEquals(3, $items[5]->getQuantity()); 38 | $this->assertEquals(100, $items[5]->getPrice()); 39 | } 40 | 41 | public function testAddExist() 42 | { 43 | $this->cart->add($this->product, 3); 44 | $this->cart->add($this->product, 4); 45 | $this->assertEquals(1, count($items = $this->cart->getItems())); 46 | $this->assertEquals(7, $items[5]->getQuantity()); 47 | } 48 | 49 | public function testRemove() 50 | { 51 | $this->cart->add($this->product, 3); 52 | $this->cart->remove(5); 53 | $this->assertEquals([], $this->cart->getItems()); 54 | } 55 | 56 | public function testClear() 57 | { 58 | $this->cart->add($this->product, 3); 59 | $this->cart->clear(); 60 | $this->assertEquals([], $this->cart->getItems()); 61 | } 62 | 63 | public function testTotalCost() 64 | { 65 | $this->cart->add($this->product, 3); 66 | $this->cart->add($this->product, 7); 67 | $this->assertEquals(1000, $this->cart->getTotalCost()); 68 | } 69 | 70 | public function testTotalCount() 71 | { 72 | $this->cart->add($this->product, 3); 73 | $this->cart->add($this->product, 7); 74 | $this->assertEquals(10, $this->cart->getTotalCount()); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/CookieStorageTest.php: -------------------------------------------------------------------------------- 1 | 'cartTest', 16 | 'productFieldId' => 'id', 17 | 'productFieldPrice' => 'price', 18 | ]; 19 | 20 | public function load() 21 | { 22 | if (!empty($this->cookie)) { 23 | return array_filter(array_map(function (array $row) { 24 | if (isset($row['id'], $row['quantity'])) { 25 | $product = new DummyProduct(); 26 | return new CartItem($product, $row['quantity'], $this->params); 27 | } 28 | return false; 29 | }, Json::decode($this->cookie->value))); 30 | } 31 | return []; 32 | } 33 | 34 | public function save(array $items) 35 | { 36 | $this->cookie = new Cookie([ 37 | 'name' => $this->params['key'], 38 | 'value' => Json::encode(array_map(function (CartItem $item) { 39 | return [ 40 | 'id' => $item->getId(), 41 | 'quantity' => $item->getQuantity(), 42 | ]; 43 | }, $items)), 44 | 'expire' => time() + 3600, 45 | ]); 46 | } 47 | 48 | public function testCreate() 49 | { 50 | $this->assertEquals([], $this->load()); 51 | } 52 | 53 | public function testStore() 54 | { 55 | $product = new DummyProduct(); 56 | $this->save([$product->id => new CartItem($product, 3, $this->params)]); 57 | 58 | /** @var CartItem[] $items */ 59 | $items = $this->load(); 60 | $this->assertEquals(1, count($items)); 61 | $this->assertNotNull($items[5]); 62 | $this->assertEquals(5, $items[5]->getId()); 63 | $this->assertEquals(3, $items[5]->getQuantity()); 64 | $this->assertEquals(100, $items[5]->getPrice()); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/SessionStorageTest.php: -------------------------------------------------------------------------------- 1 | 'cartTest', 12 | 'productFieldId' => 'id', 13 | 'productFieldPrice' => 'price', 14 | ]; 15 | 16 | public function load() 17 | { 18 | return isset($_SESSION[$this->params['key']]) ? unserialize($_SESSION[$this->params['key']]) : []; 19 | } 20 | 21 | public function save(array $items) 22 | { 23 | $_SESSION[$this->params['key']] = serialize($items); 24 | } 25 | 26 | public function testCreate() 27 | { 28 | $this->assertEquals([], $this->load()); 29 | } 30 | 31 | public function testStore() 32 | { 33 | $product = new DummyProduct(); 34 | $this->save([5 => new CartItem($product, 3, $this->params)]); 35 | 36 | /** @var CartItem[] $items */ 37 | $items = $this->load(); 38 | $this->assertEquals(1, count($items)); 39 | $this->assertNotNull($items[5]); 40 | $this->assertEquals(5, $items[5]->getId()); 41 | $this->assertEquals(3, $items[5]->getQuantity()); 42 | $this->assertEquals(100, $items[5]->getPrice()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/SimpleCalculatorTest.php: -------------------------------------------------------------------------------- 1 | 'id', 13 | 'productFieldPrice' => 'price', 14 | ]; 15 | 16 | public function testCostCalculate() 17 | { 18 | $calculator = new SimpleCalculator(); 19 | $product = new DummyProduct(); 20 | $this->assertEquals(300, $calculator->getCost([ 21 | $product->id => new CartItem($product, 3, $this->params), 22 | ])); 23 | } 24 | 25 | public function testCountCalculate() 26 | { 27 | $calculator = new SimpleCalculator(); 28 | $product = new DummyProduct(); 29 | $this->assertEquals(3, $calculator->getCount([ 30 | $product->id => new CartItem($product, 3, $this->params), 31 | ])); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | mockApplication(); 11 | } 12 | 13 | protected function tearDown() 14 | { 15 | $this->destroyApplication(); 16 | parent::tearDown(); 17 | } 18 | 19 | protected function mockApplication() 20 | { 21 | new \yii\console\Application([ 22 | 'id' => 'testapp', 23 | 'basePath' => __DIR__, 24 | 'vendorPath' => dirname(__DIR__) . '/vendor', 25 | 'components' => [ 26 | 'cart' => [ 27 | 'class' => 'devanych\cart\Cart', 28 | 'storageClass' => 'devanych\cart\tests\data\DummyStorage', 29 | 'params' => [ 30 | 'productClass' => 'devanych\cart\tests\data\DummyProduct', 31 | ], 32 | ], 33 | ], 34 | ]); 35 | } 36 | 37 | protected function destroyApplication() 38 | { 39 | \Yii::$app = null; 40 | } 41 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | params = $params; 15 | } 16 | 17 | public function load() 18 | { 19 | return $this->items; 20 | } 21 | 22 | public function save(array $items) 23 | { 24 | $this->items = $items; 25 | } 26 | } 27 | --------------------------------------------------------------------------------