├── CartManager.php ├── Changelog.md ├── Contracts ├── Cart.php ├── CartEvent.php ├── CartItem.php ├── CartManager.php └── CartState.php ├── Events ├── BaseCartEvent.php ├── CartCreated.php ├── CartDeleted.php ├── CartDeleting.php └── CartUpdated.php ├── Exceptions └── InvalidCartConfigurationException.php ├── Facades └── Cart.php ├── LICENSE.md ├── Listeners ├── AssignUserToCart.php ├── DissociateUserFromCart.php └── RestoreCurrentUsersLastActiveCart.php ├── Models ├── Cart.php ├── CartItem.php ├── CartItemProxy.php ├── CartProxy.php ├── CartState.php └── CartStateProxy.php ├── Providers ├── EventServiceProvider.php └── ModuleServiceProvider.php ├── README.md ├── composer.json └── resources ├── config └── module.php ├── database └── migrations │ ├── 2017_10_28_111947_create_carts_table.php │ ├── 2017_10_29_224033_create_cart_items_table.php │ ├── 2018_10_15_224808_add_cart_state.php │ └── 2023_01_12_123641_add_cart_item_configuration.php └── manifest.php /CartManager.php: -------------------------------------------------------------------------------- 1 | sessionKey = config(self::CONFIG_SESSION_KEY); 45 | 46 | if (empty($this->sessionKey)) { 47 | throw new InvalidCartConfigurationException( 48 | sprintf( 49 | 'Cart session key (`%s`) is empty. Please provide a valid value.', 50 | self::CONFIG_SESSION_KEY 51 | ) 52 | ); 53 | } 54 | } 55 | 56 | public function __call(string $name, array $arguments): mixed 57 | { 58 | if (null !== $this->model()) { 59 | return call_user_func([$this->model(), $name], ...$arguments); 60 | } 61 | 62 | return null; 63 | } 64 | 65 | public function getItems(): Collection 66 | { 67 | return $this->exists() ? $this->model()->getItems() : collect(); 68 | } 69 | 70 | public function itemsTotal(): float 71 | { 72 | return $this->exists() ? $this->model()->itemsTotal() : 0; 73 | } 74 | 75 | public function addItem(Buyable $product, int|float $qty = 1, array $params = []): CartItem 76 | { 77 | $cart = $this->findOrCreateCart(); 78 | 79 | $result = $cart->addItem($product, $qty, $params); 80 | $this->triggerCartUpdatedEvent(); 81 | 82 | return $result; 83 | } 84 | 85 | public function removeItem(CartItem $item): void 86 | { 87 | if ($cart = $this->model()) { 88 | $cart->removeItem($item); 89 | $this->triggerCartUpdatedEvent(); 90 | } 91 | } 92 | 93 | public function removeProduct(Buyable $product): void 94 | { 95 | if ($cart = $this->model()) { 96 | $cart->removeProduct($product); 97 | $this->triggerCartUpdatedEvent(); 98 | } 99 | } 100 | 101 | public function clear(): void 102 | { 103 | if ($cart = $this->model()) { 104 | $cart->clear(); 105 | $this->triggerCartUpdatedEvent(); 106 | } 107 | } 108 | 109 | public function itemCount(): int 110 | { 111 | return $this->exists() ? $this->model()->itemCount() : 0; 112 | } 113 | 114 | public function total(): float 115 | { 116 | return $this->exists() ? $this->model()->total() : 0; 117 | } 118 | 119 | public function exists(): bool 120 | { 121 | return (bool) $this->getCartId() && $this->model(); 122 | } 123 | 124 | public function doesNotExist(): bool 125 | { 126 | return !$this->exists(); 127 | } 128 | 129 | public function model(): ?CartContract 130 | { 131 | $id = $this->getCartId(); 132 | 133 | if ($id && $this->cart) { 134 | return $this->cart; 135 | } elseif ($id) { 136 | $this->cart = CartProxy::find($id); 137 | 138 | return $this->cart; 139 | } 140 | 141 | return null; 142 | } 143 | 144 | public function isEmpty(): bool 145 | { 146 | return 0 == $this->itemCount(); 147 | } 148 | 149 | public function isNotEmpty(): bool 150 | { 151 | return !$this->isEmpty(); 152 | } 153 | 154 | public function destroy(): void 155 | { 156 | if ($this->exists()) { 157 | Event::dispatch(new CartDeleting($this->model())); 158 | } 159 | 160 | // don't call $this->clear() as the triggered CartUpdated event may cause unwanted side effects 161 | $this->model()->clear(); 162 | $this->model()->delete(); 163 | $this->forget(); 164 | 165 | Event::dispatch(new CartDeleted()); 166 | } 167 | 168 | public function create(bool $forceCreateIfExists = false): void 169 | { 170 | if ($this->exists() && !$forceCreateIfExists) { 171 | return; 172 | } 173 | 174 | $this->createCart(); 175 | } 176 | 177 | public function getUser(): ?Authenticatable 178 | { 179 | return $this->exists() ? $this->model()->getUser() : null; 180 | } 181 | 182 | public function setUser(Authenticatable|int|string|null $user): void 183 | { 184 | if ($this->exists()) { 185 | $this->cart->setUser($user); 186 | $this->cart->save(); 187 | $this->cart->load('user'); 188 | } 189 | } 190 | 191 | public function removeUser(): void 192 | { 193 | $this->setUser(null); 194 | } 195 | 196 | public function restoreLastActiveCart($user): void 197 | { 198 | $lastActiveCart = CartProxy::ofUser($user)->actives()->latest()->first(); 199 | 200 | if ($lastActiveCart) { 201 | $this->setCartModel($lastActiveCart); 202 | $this->triggerCartUpdatedEvent(); 203 | } 204 | } 205 | 206 | public function mergeLastActiveCartWithSessionCart($user): void 207 | { 208 | /** @var Cart $lastActiveCart */ 209 | if ($lastActiveCart = CartProxy::ofUser($user)->actives()->latest()->first()) { 210 | /** @var CartItem $item */ 211 | foreach ($lastActiveCart->getItems() as $item) { 212 | $this->addItem($item->getBuyable(), $item->getQuantity()); 213 | } 214 | 215 | $lastActiveCart->delete(); 216 | $this->triggerCartUpdatedEvent(); 217 | } 218 | } 219 | 220 | public function forget(): void 221 | { 222 | $this->cart = null; 223 | session()->forget($this->sessionKey); 224 | } 225 | 226 | /** 227 | * Refreshes the underlying cart model from the database 228 | */ 229 | public function fresh(): self 230 | { 231 | if (null !== $this->cart) { 232 | $this->cart = $this->cart->fresh(); 233 | } 234 | 235 | return $this; 236 | } 237 | 238 | /** 239 | * Returns the model id of the cart for the current session 240 | * or null if it does not exist 241 | * 242 | * @return int|null 243 | */ 244 | protected function getCartId() 245 | { 246 | return session($this->sessionKey); 247 | } 248 | 249 | /** 250 | * Returns the cart model for the current session by either fetching it or creating one 251 | */ 252 | protected function findOrCreateCart(): Cart|CartContract 253 | { 254 | return $this->model() ?: $this->createCart(); 255 | } 256 | 257 | /** 258 | * Creates a new cart model and saves its id in the session 259 | */ 260 | protected function createCart(): Cart|CartContract 261 | { 262 | if (config('vanilo.cart.auto_assign_user') && Auth::check()) { 263 | $attributes = [ 264 | 'user_id' => Auth::user()->id 265 | ]; 266 | } 267 | 268 | $model = $this->setCartModel(CartProxy::create($attributes ?? [])); 269 | 270 | Event::dispatch(new CartCreated($model)); 271 | 272 | return $model; 273 | } 274 | 275 | protected function setCartModel(CartContract $cart): CartContract 276 | { 277 | $this->cart = $cart; 278 | 279 | session([$this->sessionKey => $this->cart->id]); 280 | 281 | return $this->cart; 282 | } 283 | 284 | protected function triggerCartUpdatedEvent(): void 285 | { 286 | if ($this->exists()) { 287 | Event::dispatch(new CartUpdated($this->model())); 288 | } 289 | } 290 | } 291 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Vanilo Cart Module Changelog 2 | 3 | ## 4.x Series 4 | 5 | ## 4.2.0 6 | ##### 2024-12-15 7 | 8 | - Changed `CartItem::hasConfiguration()` to return false on empty arrays as well 9 | - Fixed an error when attempting to remove a product from the cart which is not in the by [xujiongze](https://github.com/xujiongze) in [#188](https://github.com/vanilophp/framework/pull/188) 10 | 11 | ## 4.1.0 12 | ##### 2024-07-11 13 | 14 | - Bump module version to mainline (no change) 15 | 16 | ## 4.0.0 17 | ##### 2024-04-25 18 | 19 | - Dropped PHP 8.0 & PHP 8.1 Support 20 | - Dropped Laravel 9 Support 21 | - Dropped Enum v3 Support 22 | - Added PHP 8.3 Support 23 | - Added Laravel 11 Support 24 | - Added Cart item configuration support (different configurations constitute separate cart items) to the `Cart::addItem()` method 25 | - Changed minimum Laravel version to v10.43 26 | - Changed minimal Enum requirement to v4.2 27 | - Removed the throwing of `CartUpdated` event when destroying a cart (`CartDeleting` and `CartDeleted` remains) 28 | - BC: Changed the `CheckoutSubjectItem` interface into Configurable & Schematized 29 | - BC: Added argument and return types to all `Cart` and `CartManager` interface methods 30 | 31 | ## 3.x Series 32 | 33 | ## 3.8.0 34 | ##### 2023-05-24 35 | 36 | - Bump module version to mainline (no change) 37 | 38 | ## 3.7.0 39 | ##### 2023-04-04 40 | 41 | - Bump module version to mainline (no change) 42 | 43 | ## 3.6.0 44 | ##### 2023-03-07 45 | 46 | - Added Laravel 10 support 47 | - Added the `CartCreated`, `CartUpdated`, `CartDeleted` and `CartDeleting` events 48 | 49 | ## 3.5.0 50 | ##### 2023-02-23 51 | 52 | - Bump module version to mainline (no change) 53 | 54 | ## 3.4.0 55 | ##### 2023-01-25 56 | 57 | - Added `Configurable` to the `CartItem` model (incl. implementing the interface) 58 | 59 | ## 3.3.0 60 | ##### 2023-01-05 61 | 62 | - Added final PHP 8.2 support 63 | 64 | ## 3.2.0 65 | ##### 2022-12-08 66 | 67 | - Added `Cart::fresh()` method to the Cart facade 68 | - Changed minimum Concord version requirement to v1.12 69 | 70 | ## 3.1.0 71 | ##### 2022-11-07 72 | 73 | - Added Enum 4.0 Support 74 | - Added `__call` to `CartManager` that proxies unhandled calls to the underlying cart model 75 | - Changed minimum Laravel requirement to 9.2 76 | - Changed minimum Konekt module requirements to: 77 | - Concord: 1.11 78 | - Enum: 3.1.1 79 | 80 | ## 3.0.1 81 | ##### 2022-05-22 82 | 83 | - Bump module version to mainline (no change) 84 | 85 | ## 3.0.0 86 | ##### 2022-02-28 87 | 88 | - Added Laravel 9 support 89 | - Added PHP 8.1 support 90 | - Dropped PHP 7.4 Support 91 | - Dropped Laravel 6-8 Support 92 | - Removed Admin from "Framework" - it is available as an optional separate package see [vanilo/admin](https://github.com/vanilophp/admin) 93 | - Minimum Laravel version is 8.22.1. [See GHSA-3p32-j457-pg5x](https://github.com/advisories/GHSA-3p32-j457-pg5x) 94 | 95 | 96 | --- 97 | 98 | ## 2.x Series 99 | 100 | ### 2.2.0 101 | ##### 2021-09-11 102 | 103 | - Changed internal CS ruleset from PSR-2 to PSR-12 104 | - Dropped PHP 7.3 support 105 | 106 | ### 2.1.1 107 | ##### 2020-12-31 108 | 109 | - Added PHP 8 support 110 | - Changed CI from Travis to Github 111 | - Only works with Vanilo 2.1+ modules 112 | 113 | ### 2.1.0 114 | ##### 2020-10-27 115 | 116 | - Added configuration option to explicitly define the cart's user model class 117 | - Works with Vanilo 2.0 modules 118 | 119 | ### 2.0.0 120 | ##### 2020-10-11 121 | 122 | - BC: interfaces comply with vanilo/contracts v2 123 | - BC: Upgrade to Enum v3 124 | - Added Laravel 8 support 125 | - Dropped Laravel 5 support 126 | - Dropped PHP 7.2 support 127 | 128 | ## 1.x Series 129 | 130 | ### 1.2.0 131 | ##### 2020-03-29 132 | 133 | - Added Laravel 7 Support 134 | - Added PHP 7.4 support 135 | - Dropped PHP 7.1 support 136 | 137 | ### 1.1.1 138 | ##### 2019-12-21 139 | 140 | - Fixed bug with cart id stuck in session without matching DB entry. 141 | 142 | ### 1.1.0 143 | ##### 2019-11-25 144 | 145 | - Added Laravel 6 Support 146 | - Dropped Laravel 5.4 Support 147 | 148 | ### 1.0.0 149 | ##### 2019-11-11 150 | 151 | - Added protection against missing cart session config key value 152 | - Added merge cart feature on login 153 | 154 | ## 0.5 Series 155 | 156 | ### 0.5.1 157 | ##### 2019-03-17 158 | 159 | - Complete Laravel 5.8 compatibility (likely works with 0.4.0 & 0.5.0 as well) 160 | - PHP 7.0 support has been dropped 161 | 162 | ### 0.5.0 163 | ##### 2019-02-11 164 | 165 | - No change, version has been bumped for v0.5 series 166 | 167 | ## 0.4 Series 168 | 169 | ### 0.4.0 170 | ##### 2018-11-12 171 | 172 | - Possibility to preserve cart for users (across logins) feature 173 | - Laravel 5.7 compatibility 174 | - Tested with PHP 7.3 175 | 176 | ## 0.3 Series 177 | 178 | ### 0.3.0 179 | ##### 2018-08-11 180 | 181 | - Custom product attributes can be passed/configured when adding cart items 182 | - Works with product images 183 | - Test suite improvements for Laravel 5.4 compatibility 184 | - Doc improvements 185 | 186 | ## 0.2 Series 187 | 188 | ### 0.2.0 189 | ##### 2018-02-19 190 | 191 | - Cart user handling works 192 | - Laravel 5.6 compatible 193 | 194 | 195 | ## 0.1 Series 196 | 197 | ### 0.1.0 198 | ##### 2017-12-11 199 | 200 | - 🐣 -> 🛂 -> 🤦 -> 💁 201 | -------------------------------------------------------------------------------- /Contracts/Cart.php: -------------------------------------------------------------------------------- 1 | cart; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Events/CartCreated.php: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | > of this software and associated documentation files (the "Software"), to deal 7 | > in the Software without restriction, including without limitation the rights 8 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | > copies of the Software, and to permit persons to whom the Software is 10 | > furnished to do so, subject to the following conditions: 11 | > 12 | > The above copyright notice and this permission notice shall be included in 13 | > all copies or substantial portions of the Software. 14 | > 15 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | > THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Listeners/AssignUserToCart.php: -------------------------------------------------------------------------------- 1 | id == $event->user->id) { 30 | return; // Don't associate to the same user again 31 | } 32 | Cart::setUser($event->user); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Listeners/DissociateUserFromCart.php: -------------------------------------------------------------------------------- 1 | 'CartStateProxy@enumClass', 38 | ]; 39 | 40 | public function items(): HasMany 41 | { 42 | return $this->hasMany(CartItemProxy::modelClass(), 'cart_id', 'id'); 43 | } 44 | 45 | public function getItems(): Collection 46 | { 47 | return $this->items; 48 | } 49 | 50 | public function itemCount(): int 51 | { 52 | return (int) $this->items->sum('quantity'); 53 | } 54 | 55 | public function addItem(Buyable $product, int|float $qty = 1, array $params = []): CartItemContract 56 | { 57 | $item = $this->resolveCartItem($product, $params); 58 | 59 | if ($item) { 60 | $item->quantity += $qty; 61 | $item->save(); 62 | } else { 63 | $item = $this->items()->create( 64 | array_merge( 65 | $this->getDefaultCartItemAttributes($product, $qty), 66 | $this->getExtraProductMergeAttributes($product), 67 | $params['attributes'] ?? [] 68 | ) 69 | ); 70 | } 71 | 72 | $this->load('items'); 73 | 74 | return $item; 75 | } 76 | 77 | public function removeItem(CartItemContract $item): void 78 | { 79 | if ($item instanceof Model) { 80 | $item->delete(); 81 | } 82 | 83 | $this->load('items'); 84 | } 85 | 86 | /** 87 | * @inheritDoc 88 | */ 89 | public function removeProduct(Buyable $product): void 90 | { 91 | $item = $this->items()->ofCart($this)->byProduct($product)->first(); 92 | if ($item) { 93 | $this->removeItem($item); 94 | } 95 | } 96 | 97 | /** 98 | * @inheritDoc 99 | */ 100 | public function clear(): void 101 | { 102 | $this->items()->ofCart($this)->delete(); 103 | 104 | $this->load('items'); 105 | } 106 | 107 | public function total(): float 108 | { 109 | return $this->items->sum('total'); 110 | } 111 | 112 | public function itemsTotal(): float 113 | { 114 | return $this->items->sum('total'); 115 | } 116 | 117 | /** 118 | * The cart's user relationship 119 | * 120 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 121 | */ 122 | public function user() 123 | { 124 | $userModel = config('vanilo.cart.user.model') ?: config('auth.providers.users.model'); 125 | 126 | return $this->belongsTo($userModel); 127 | } 128 | 129 | public function getUser(): ?Authenticatable 130 | { 131 | return $this->user; 132 | } 133 | 134 | public function setUser(Authenticatable|int|string|null $user): void 135 | { 136 | if ($user instanceof Authenticatable) { 137 | $user = $user->id; 138 | } 139 | 140 | $this->user_id = $user; 141 | } 142 | 143 | public function scopeActives($query) 144 | { 145 | return $query->whereIn('state', CartStateProxy::getActiveStates()); 146 | } 147 | 148 | public function scopeOfUser($query, $user) 149 | { 150 | return $query->where('user_id', is_object($user) ? $user->id : $user); 151 | } 152 | 153 | protected function resolveCartItem(Buyable $buyable, array $parameters): ?CartItemContract 154 | { 155 | /** @var Collection $existingCartItems */ 156 | $existingCartItems = $this->items()->ofCart($this)->byProduct($buyable)->get(); 157 | if ($existingCartItems->isEmpty()) { 158 | return null; 159 | } 160 | 161 | $itemConfig = Arr::get($parameters, 'attributes.configuration'); 162 | 163 | if (1 === $existingCartItems->count()) { 164 | $item = $this->items()->ofCart($this)->byProduct($buyable)->first(); 165 | 166 | return $this->configurationsMatch($item->configuration(), $itemConfig) ? $item : null; 167 | } 168 | 169 | foreach ($existingCartItems as $item) { 170 | if ($this->configurationsMatch($item->configuration(), $itemConfig)) { 171 | return $item; 172 | } 173 | } 174 | 175 | return null; 176 | } 177 | 178 | protected function configurationsMatch(?array $config1, ?array $config2): bool 179 | { 180 | if (empty($config1) && empty($config2)) { 181 | return true; 182 | } elseif (empty($config1) && !empty($config2)) { 183 | return false; 184 | } elseif (empty($config2) && !empty($config1)) { 185 | return false; 186 | } 187 | 188 | if (array_is_list($config1)) { 189 | if (!array_is_list($config2)) { 190 | return false; 191 | } 192 | 193 | return empty(array_diff($config1, $config2)) && empty(array_diff($config2, $config1)); 194 | } else { //Config 1 is associative 195 | if (array_is_list($config2)) { 196 | return false; 197 | } 198 | 199 | return empty(array_diff_assoc($config1, $config2)) && empty(array_diff_assoc($config2, $config1)); 200 | } 201 | 202 | return false; 203 | } 204 | 205 | /** 206 | * Returns the default attributes of a Buyable for a cart item 207 | * 208 | * @param Buyable $product 209 | * @param integer $qty 210 | * 211 | * @return array 212 | */ 213 | protected function getDefaultCartItemAttributes(Buyable $product, $qty) 214 | { 215 | return [ 216 | 'product_type' => $product->morphTypeName(), 217 | 'product_id' => $product->getId(), 218 | 'quantity' => $qty, 219 | 'price' => $product->getPrice(), 220 | ]; 221 | } 222 | 223 | /** 224 | * Returns the extra product merge attributes for cart_items based on the config 225 | * 226 | * @param Buyable $product 227 | * 228 | * @throws InvalidCartConfigurationException 229 | * 230 | * @return array 231 | */ 232 | protected function getExtraProductMergeAttributes(Buyable $product) 233 | { 234 | $result = []; 235 | $cfg = config(self::EXTRA_PRODUCT_MERGE_ATTRIBUTES_CONFIG_KEY, []); 236 | 237 | if (!is_array($cfg)) { 238 | throw new InvalidCartConfigurationException( 239 | sprintf( 240 | 'The value of `%s` configuration must be an array', 241 | self::EXTRA_PRODUCT_MERGE_ATTRIBUTES_CONFIG_KEY 242 | ) 243 | ); 244 | } 245 | 246 | foreach ($cfg as $attribute) { 247 | if (!is_string($attribute)) { 248 | throw new InvalidCartConfigurationException( 249 | sprintf( 250 | 'The configuration `%s` can only contain an array of strings, `%s` given', 251 | self::EXTRA_PRODUCT_MERGE_ATTRIBUTES_CONFIG_KEY, 252 | gettype($attribute) 253 | ) 254 | ); 255 | } 256 | 257 | $result[$attribute] = $product->{$attribute}; 258 | } 259 | 260 | return $result; 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /Models/CartItem.php: -------------------------------------------------------------------------------- 1 | 'json', 37 | ]; 38 | 39 | public function product() 40 | { 41 | return $this->morphTo(); 42 | } 43 | 44 | /** 45 | * @inheritDoc 46 | */ 47 | public function getBuyable(): Buyable 48 | { 49 | return $this->product; 50 | } 51 | 52 | /** 53 | * @inheritDoc 54 | */ 55 | public function getQuantity(): int 56 | { 57 | return (int) $this->quantity; 58 | } 59 | 60 | /** 61 | * @inheritDoc 62 | */ 63 | public function total(): float 64 | { 65 | return $this->price * $this->quantity; 66 | } 67 | 68 | /** 69 | * Property accessor alias to the total() method 70 | * 71 | * @return float 72 | */ 73 | public function getTotalAttribute() 74 | { 75 | return $this->total(); 76 | } 77 | 78 | /** 79 | * Scope to query items of a cart 80 | * 81 | * @param \Illuminate\Database\Eloquent\Builder $query 82 | * @param mixed $cart Cart object or cart id 83 | * 84 | * @return \Illuminate\Database\Eloquent\Builder 85 | */ 86 | public function scopeOfCart($query, $cart) 87 | { 88 | $cartId = is_object($cart) ? $cart->id : $cart; 89 | 90 | return $query->where('cart_id', $cartId); 91 | } 92 | 93 | /** 94 | * Scope to query items by product (Buyable) 95 | * 96 | * @param Builder $query 97 | * @param Buyable $product 98 | * 99 | * @return Builder 100 | */ 101 | public function scopeByProduct($query, Buyable $product) 102 | { 103 | return $query->where([ 104 | ['product_id', '=', $product->getId()], 105 | ['product_type', '=', $product->morphTypeName()] 106 | ]); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Models/CartItemProxy.php: -------------------------------------------------------------------------------- 1 | value, static::$activeStates); 38 | } 39 | 40 | /** 41 | * @inheritDoc 42 | */ 43 | public static function getActiveStates(): array 44 | { 45 | return static::$activeStates; 46 | } 47 | 48 | protected static function boot() 49 | { 50 | static::$labels = [ 51 | self::ACTIVE => __('Active'), 52 | self::CHECKOUT => __('Checkout'), 53 | self::COMPLETED => __('Completed'), 54 | self::ABANDONDED => __('Abandoned') 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Models/CartStateProxy.php: -------------------------------------------------------------------------------- 1 | [ 30 | AssignUserToCart::class, 31 | RestoreCurrentUsersLastActiveCart::class, 32 | ], 33 | Authenticated::class => [ 34 | AssignUserToCart::class, 35 | RestoreCurrentUsersLastActiveCart::class, 36 | ], 37 | Logout::class => [ 38 | DissociateUserFromCart::class 39 | ], 40 | Lockout::class => [ 41 | DissociateUserFromCart::class 42 | ] 43 | ]; 44 | } 45 | -------------------------------------------------------------------------------- /Providers/ModuleServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind(CartManagerContract::class, CartManager::class); 40 | 41 | $this->app->singleton('vanilo.cart', function ($app) { 42 | return $app->make(CartManagerContract::class); 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vanilo Cart Module 2 | 3 | [![Tests](https://img.shields.io/github/actions/workflow/status/vanilophp/cart/tests.yml?branch=master&style=flat-square)](https://github.com/vanilophp/cart/actions?query=workflow%3Atests) 4 | [![Packagist version](https://img.shields.io/packagist/v/vanilo/cart.svg?style=flat-square)](https://packagist.org/packages/vanilo/cart) 5 | [![Packagist downloads](https://img.shields.io/packagist/dt/vanilo/cart.svg?style=flat-square)](https://packagist.org/packages/vanilo/cart) 6 | [![MIT Software License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE.md) 7 | 8 | This is the standalone cart module of the [Vanilo E-commerce Framework](https://vanilo.io). 9 | 10 | ## Installation 11 | 12 | (As Standalone Component) 13 | 14 | 1. `composer require vanilo/cart` 15 | 2. `php artisan vendor:publish --provider="Konekt\Concord\ConcordServiceProvider"` 16 | 3. Add `Vanilo\Cart\Providers\ModuleServiceProvider::class` to modules in `config/concord.php` 17 | 4. `php artisan migrate` 18 | 19 | ## Usage 20 | 21 | See the [Vanilo Cart Documentation](https://vanilo.io/docs/master/cart) for more details. 22 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilo/cart", 3 | "description": "Vanilo Cart Module", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": ["cart", "ecommerce", "vanilo", "laravel"], 7 | "support": { 8 | "issues": "https://github.com/vanilophp/framework/issues", 9 | "source": "https://github.com/vanilophp/cart" 10 | }, 11 | "minimum-stability": "dev", 12 | "prefer-stable": true, 13 | "authors": [ 14 | { 15 | "name": "Attila Fulop", 16 | "homepage": "https://github.com/fulopattila122" 17 | } 18 | ], 19 | "require": { 20 | "php": "^8.2", 21 | "konekt/concord": "^1.13", 22 | "konekt/enum": "^4.2", 23 | "laravel/framework": "^10.43|^11.0", 24 | "vanilo/contracts": "^4.0", 25 | "vanilo/support": "^4.2" 26 | }, 27 | "require-dev": { 28 | "ext-sqlite3": "*", 29 | "phpunit/phpunit": "^10.0", 30 | "orchestra/testbench": "^8.0|^9.0", 31 | "laravel/legacy-factories": "^1.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { "Vanilo\\Cart\\": "" } 35 | }, 36 | "extra": { 37 | "laravel": { 38 | "aliases": { 39 | "Cart": "Vanilo\\Cart\\Facades\\Cart" 40 | } 41 | }, 42 | "branch-alias": { 43 | "dev-master": "5.0.x-dev" 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /resources/config/module.php: -------------------------------------------------------------------------------- 1 | true, 7 | 'user' => [ 8 | 'model' => null, // Leave null to use config('auth.providers.users.model'): default of v0.1 - v2.0 9 | ], 10 | 'session_key' => 'vanilo_cart', // The session key where the cart id gets saved 11 | 'auto_destroy' => false, // Whether to immediately delete carts with 0 items 12 | 'auto_assign_user' => true, // Whether to automatically set the user_id on new carts (based on Auth::user()) 13 | 'preserve_for_user' => false, // Whether to keep and restore user carts across logins and devices 14 | 'merge_duplicates' => false, // Whether to merge carts if `preserve_for_user` is enabled, user logs in and the session contains another cart 15 | 'items' => [ 16 | 'extra_product_attributes_to_merge' => [] 17 | ] 18 | ]; 19 | -------------------------------------------------------------------------------- /resources/database/migrations/2017_10_28_111947_create_carts_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 19 | $table->integer('user_id')->unsigned()->nullable(); 20 | $table->timestamps(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | * @return void 28 | */ 29 | public function down() 30 | { 31 | Schema::drop('carts'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /resources/database/migrations/2017_10_29_224033_create_cart_items_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 19 | $table->integer('cart_id')->unsigned(); 20 | $table->string('product_type'); 21 | $table->integer('product_id')->unsigned(); 22 | $table->integer('quantity')->unsigned(); 23 | $table->decimal('price', 15, 4); 24 | $table->timestamps(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::drop('cart_items'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /resources/database/migrations/2018_10_15_224808_add_cart_state.php: -------------------------------------------------------------------------------- 1 | string('state')->default(CartStateProxy::defaultValue()); 20 | }); 21 | } 22 | 23 | /** 24 | * Reverse the migrations. 25 | * 26 | * @return void 27 | */ 28 | public function down() 29 | { 30 | Schema::table('carts', function (Blueprint $table) { 31 | $table->dropColumn('state'); 32 | }); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /resources/database/migrations/2023_01_12_123641_add_cart_item_configuration.php: -------------------------------------------------------------------------------- 1 | json('configuration')->nullable()->after('price'); 13 | }); 14 | } 15 | 16 | public function down() 17 | { 18 | Schema::table('cart_items', function (Blueprint $table) { 19 | $table->dropColumn('configuration'); 20 | }); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /resources/manifest.php: -------------------------------------------------------------------------------- 1 | 'Vanilo Cart Module', 7 | 'version' => '4.2.0' 8 | ]; 9 | --------------------------------------------------------------------------------