├── LICENSE ├── README.md ├── composer.json └── src ├── Action.php ├── ActionsContainer.php ├── Cart.php ├── CartServiceProvider.php ├── Config └── config.php ├── Container.php ├── Contracts ├── ActionHandler.php ├── CartNode.php └── UseCartable.php ├── Details.php ├── Exceptions ├── InvalidArgumentException.php ├── InvalidAssociatedException.php ├── InvalidCartNameException.php ├── InvalidHashException.php ├── InvalidModelException.php └── UnknownCreatorException.php ├── Facades └── Cart.php ├── Helpers └── helpers.php ├── Item.php ├── ItemsContainer.php ├── Tax.php ├── TaxesContainer.php └── Traits ├── BackToCreator.php ├── CanApplyAction.php ├── CanBeCartNode.php ├── CanUseCart.php ├── CollectionForgetAll.php └── FireEvent.php /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Anh Vũ Đỗ 4 | 5 | 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 all 13 | 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 THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Cart 2 | 3 | [![Run tests](https://github.com/JackieDo/Laravel-Cart/actions/workflows/run-tests.yml/badge.svg?branch=v3.0)](https://github.com/JackieDo/Laravel-Cart/actions/workflows/run-tests.yml) 4 | [![Build Status](https://api.travis-ci.org/JackieDo/Laravel-Cart.svg?branch=v3.0)](https://travis-ci.org/JackieDo/Laravel-Cart) 5 | [![Total Downloads](https://poser.pugx.org/jackiedo/cart/downloads)](https://packagist.org/packages/jackiedo/cart) 6 | [![Latest Stable Version](https://poser.pugx.org/jackiedo/cart/v/stable)](https://packagist.org/packages/jackiedo/cart) 7 | [![License](https://poser.pugx.org/jackiedo/cart/license)](https://packagist.org/packages/jackiedo/cart) 8 | 9 | Laravel Cart is a package used to create and manage carts (such as shopping, recently viewed, compared items...) in Laravel application. 10 | 11 | ## Features 12 | - Session based system. 13 | - Support multiple cart instances. 14 | - Classification of commercial and non-commercial carts. 15 | - Grouping the carts. 16 | - Quickly insert items with your own item models. 17 | - Taxation on the cart level (with built-in taxing system). 18 | - Applying actions on the cart and item level (such as discount, service charge, shipping cost...). 19 | - Exporting details as Laravel Collection. 20 | - Allows storage of extended information. 21 | - Control of firing events. 22 | 23 | ## Versions and compatibility 24 | Currently, the Laravel Cart has three branches that are compatible with the following versions of Laravel: 25 | 26 | | Branch | Tag releases | Laravel version | 27 | | ---------------------------------------------------------- | ------------ | ---------------- | 28 | | [v1.0](https://github.com/JackieDo/Laravel-Cart/tree/v1.0) | 1.* | 4.x only | 29 | | [v2.0](https://github.com/JackieDo/Laravel-Cart/tree/v2.0) | 2.* | 5.x only | 30 | | [v3.0](https://github.com/JackieDo/Laravel-Cart/tree/v3.0) | 3.* | 5.x or above | 31 | 32 | Currently, versions `v1.0` and `v2.0` are no longer supported. Version `v3.0` was created with more advanced features, and has a completely different way of working from the old version. 33 | 34 | ## Important note (*) 35 | Version 3.0 has a different structure and working method from previous versions. Therefore, if you have used previous versions and do not want to change or want to learn new ways of working, I recommend that you do not install this version. Staying with the old version, it doesn't give you any new features, but gives you safety. 36 | 37 | On the contrary, if you choose version 3.0 to work, you will own particularly useful features that previous versions did not have. It is important that you read the documentation carefully to work properly. 38 | 39 | ## Documentation 40 | You can find documentation for version `v3.0` [here](https://jackiedo.github.io/Laravel-Cart). Documentations for older versions, please see the respective branches. 41 | 42 | ## Testing 43 | The package has been tested through over 120 test cases with GitHub Actions from PHP version 7.1 (Laravel 5.8) to 8.2 (Laravel 10.x). Detailed information about test cases please see [here](https://github.com/JackieDo/Laravel-Cart/actions/workflows/run-tests.yml). 44 | 45 | ## License 46 | [MIT](LICENSE) © Jackie Do 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jackiedo/cart", 3 | "description": "A package used to create and manage carts (such as shopping, recently viewed, compared items...) in Laravel application.", 4 | "keywords": ["cart", "shoppingcart", "shopping", "laravel"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Jackie Do", 9 | "email": "anhvudo@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": ">=5.6.0", 14 | "laravel/framework": "^10.0|^9.0|^8.0|^7.0|^6.0|^5.0" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^9.0|^8.0|^7.0|^6.0|^5.0|^4.0", 18 | "mockery/mockery": "^1.0|^0.9", 19 | "orchestra/testbench": "^8.0|^7.0|^6.0|^5.0|^4.0|^3.0" 20 | }, 21 | "autoload": { 22 | "files": [ 23 | "src/Helpers/helpers.php" 24 | ], 25 | "psr-4": { 26 | "Jackiedo\\Cart\\": "src" 27 | } 28 | }, 29 | "extra": { 30 | "laravel": { 31 | "providers": [ 32 | "Jackiedo\\Cart\\CartServiceProvider" 33 | ], 34 | "aliases": { 35 | "Cart": "Jackiedo\\Cart\\Facades\\Cart" 36 | } 37 | } 38 | }, 39 | "minimum-stability": "stable" 40 | } 41 | -------------------------------------------------------------------------------- /src/Action.php: -------------------------------------------------------------------------------- 1 | 'unknown', 22 | 'id' => null, 23 | 'title' => null, 24 | 'target' => null, 25 | 'value' => 0, 26 | 'rules' => [], 27 | 'extra_info' => [], 28 | ]; 29 | 30 | /** 31 | * The name of the accepted class is the creator. 32 | * 33 | * @var array 34 | */ 35 | protected $acceptedCreators = [ 36 | ActionsContainer::class, 37 | ]; 38 | 39 | /** 40 | * Indicates whether this action belongs to a taxable cart. 41 | * 42 | * @var bool 43 | */ 44 | protected $enabledBuiltinTax = false; 45 | 46 | /** 47 | * Stores the number used to sort. 48 | * 49 | * @var int 50 | */ 51 | protected $orderNumber; 52 | 53 | /** 54 | * The constructor. 55 | * 56 | * @param array $attributes The action attributes 57 | */ 58 | public function __construct(array $attributes = []) 59 | { 60 | // Stores the creator 61 | $this->storeCreator(0, function ($creator, $caller) { 62 | $cart = $this->getCart(); 63 | $this->enabledBuiltinTax = $cart->isEnabledBuiltinTax(); 64 | $this->attributes['rules'] = $cart->getConfig('default_action_rules', []); 65 | }); 66 | 67 | // Initialize attributes 68 | $this->initAttributes($attributes); 69 | } 70 | 71 | /** 72 | * Update attributes of this action instance. 73 | * 74 | * @param array $attributes The new attributes 75 | * @param bool $withEvent Enable firing the event 76 | * 77 | * @return $this 78 | */ 79 | public function update(array $attributes = [], $withEvent = true) 80 | { 81 | // Determines the caller that called this method 82 | $caller = getCaller(); 83 | $callerClass = Arr::get($caller, 'class'); 84 | $creator = $this->getCreator(); 85 | 86 | // If the caller is not the creator of this instance 87 | if ($callerClass !== get_class($creator)) { 88 | return $creator->updateAction($this->getHash(), $attributes, $withEvent); 89 | } 90 | 91 | // Filter the allowed attributes to be updated 92 | $attributes = Arr::only($attributes, ['title', 'group', 'value', 'rules', 'extra_info']); 93 | 94 | // Validate the input 95 | $this->validate($attributes); 96 | 97 | // Stores the input into attributes 98 | $this->setAttributes($attributes); 99 | 100 | return $this; 101 | } 102 | 103 | /** 104 | * Get details of the action as a collection. 105 | * 106 | * @return \Jackiedo\Cart\Details 107 | */ 108 | public function getDetails() 109 | { 110 | $details = [ 111 | 'hash' => $this->getHash(), 112 | 'group' => $this->getGroup(), 113 | 'id' => $this->getId(), 114 | 'title' => $this->getTitle(), 115 | 'target' => $this->getTarget(), 116 | 'value' => $this->getValue(), 117 | 'rules' => new Details($this->getRules()), 118 | ]; 119 | 120 | $details['enabled'] = $this->isEnabled(); 121 | 122 | if ($this->enabledBuiltinTax) { 123 | $details['taxable'] = $this->isTaxable(); 124 | } 125 | 126 | $details['amount'] = $this->getAmount(); 127 | $details['extra_info'] = new Details($this->getExtraInfo()); 128 | 129 | return new Details($details); 130 | } 131 | 132 | /** 133 | * Determines which values ​​to filter. 134 | * 135 | * @return array 136 | */ 137 | public function getFilterValues() 138 | { 139 | return array_merge([ 140 | 'hash' => $this->getHash(), 141 | 'rules' => $this->getRules(), 142 | 'enabled' => $this->isEnabled(), 143 | 'taxable' => $this->isTaxable(), 144 | ], Arr::only($this->attributes, ['group', 'id', 'title', 'value', 'extra_info'])); 145 | } 146 | 147 | /** 148 | * Return the unique identifier of this action. 149 | * 150 | * @return string 151 | */ 152 | public function getHash() 153 | { 154 | return 'action_' . md5($this->attributes['id'] . $this->attributes['group']); 155 | } 156 | 157 | /** 158 | * Get the id used to sort. 159 | * 160 | * @return string 161 | */ 162 | public function getOrderId() 163 | { 164 | $groupsOrder = $this->getConfig('action_groups_order', []); 165 | $thisGroupOrder = array_search($this->attributes['group'], $groupsOrder); 166 | 167 | if (false !== $thisGroupOrder) { 168 | return '1.' . $thisGroupOrder . '.' . $this->orderNumber; 169 | } 170 | 171 | return '2.0.' . $this->orderNumber; 172 | } 173 | 174 | /** 175 | * Get the formatted rules of this actions. 176 | * 177 | * @param null|string $rule The specific rule or a set of rules 178 | * @param mixed $default The return value if the rule does not exist 179 | * 180 | * @return mixed 181 | */ 182 | public function getRules($rule = null, $default = null) 183 | { 184 | // Get original rules 185 | $originalRules = $this->attributes['rules']; 186 | 187 | // If rules is instance of ActionHandler 188 | if ($originalRules instanceof ActionHandler) { 189 | $originalRules = call_user_func_array([$originalRules, 'cartActionHandle'], [$this]); 190 | $originalRules = array_merge($this->getConfig('default_action_rules', []), $originalRules); 191 | } 192 | 193 | // Format the rules 194 | $rules = [ 195 | 'enable' => (bool) Arr::get($originalRules, 'enable', true), 196 | 'taxable' => (bool) Arr::get($originalRules, 'taxable', true), 197 | 'allow_others_disable' => (bool) Arr::get($originalRules, 'allow_others_disable', true), 198 | 'disable_others' => Arr::get($originalRules, 'disable_others'), 199 | 'include_calculations' => Arr::get($originalRules, 'include_calculations'), 200 | 'max_amount' => Arr::get($originalRules, 'max_amount'), 201 | 'min_amount' => Arr::get($originalRules, 'min_amount'), 202 | ]; 203 | 204 | if (!in_array($rules['disable_others'], [null, 'previous_actions', 'same_group_previous_actions', 'previous_groups'])) { 205 | $rules['disable_others'] = null; 206 | } 207 | 208 | if (!in_array($rules['include_calculations'], [null, 'previous_actions', 'same_group_previous_actions', 'previous_groups'])) { 209 | $rules['include_calculations'] = null; 210 | } 211 | 212 | if (!is_null($rules['max_amount'])) { 213 | $rules['max_amount'] = floatval($rules['max_amount']); 214 | } 215 | 216 | if (!is_null($rules['min_amount'])) { 217 | $rules['min_amount'] = floatval($rules['min_amount']); 218 | } 219 | 220 | // Return result 221 | if (is_null($rule)) { 222 | return $rules; 223 | } 224 | 225 | if (is_array($rule)) { 226 | return Arr::only($rules, $rule); 227 | } 228 | 229 | return Arr::get($rules, $rule, $default); 230 | } 231 | 232 | /** 233 | * Indicates whether this action is taxable. 234 | * 235 | * @return bool Return true if parent node is taxable item 236 | * and the taxable rule is true 237 | */ 238 | public function isTaxable() 239 | { 240 | if (!$this->enabledBuiltinTax) { 241 | return $this->getRules('taxable', true); 242 | } 243 | 244 | $parentNode = $this->getParentNode(); 245 | 246 | if ($parentNode instanceof Item && !$parentNode->isTaxable()) { 247 | return false; 248 | } 249 | 250 | return $this->getRules('taxable', true); 251 | } 252 | 253 | /** 254 | * Determines if this action is self enabled through the rules attribute. 255 | * 256 | * @return bool 257 | */ 258 | public function isSelfActivated() 259 | { 260 | return $this->getRules('enable', true); 261 | } 262 | 263 | /** 264 | * Determines if this action is disabled by one of the following action. 265 | * 266 | * @return bool 267 | */ 268 | public function isDeactivated() 269 | { 270 | // If this action do not allow disabled 271 | if (!$this->getRules('allow_others_disable', true)) { 272 | return false; 273 | } 274 | 275 | // Get the behind actions that could be deactivator of this action 276 | $container = $this->getCreator(); 277 | $deactivators = $container->filter(function ($action) { 278 | if ($this->isPreviousOf($action) && $action->isEnabled()) { 279 | $actionDisableRule = $action->getRules('disable_others'); 280 | 281 | if ('previous_actions' === $actionDisableRule) { 282 | return true; 283 | } 284 | 285 | if ('same_group_previous_actions' === $actionDisableRule && $this->isSameGroupAs($action)) { 286 | return true; 287 | } 288 | 289 | if ('previous_groups' === $actionDisableRule && !$this->isSameGroupAs($action)) { 290 | return true; 291 | } 292 | 293 | if (is_array($actionDisableRule) && in_array($this->getGroup(), $actionDisableRule)) { 294 | return true; 295 | } 296 | } 297 | 298 | return false; 299 | }); 300 | 301 | // If there does not exist any deactivator action 302 | if ($deactivators->isEmpty()) { 303 | return false; 304 | } 305 | 306 | // Otherwise 307 | return true; 308 | } 309 | 310 | /** 311 | * Indicates whether this action is enabled. 312 | * 313 | * @return bool 314 | */ 315 | public function isEnabled() 316 | { 317 | return $this->isSelfActivated() && !$this->isDeactivated(); 318 | } 319 | 320 | /** 321 | * Determines if this action takes place before a specific action. 322 | * 323 | * @param \Jackiedo\Cart\Action $action The specific action 324 | * 325 | * @return bool 326 | */ 327 | public function isPreviousOf(Action $action) 328 | { 329 | return ($this->getHash() != $action->getHash()) && ($this->getOrderId() < $action->getOrderId()); 330 | } 331 | 332 | /** 333 | * Determines if this action takes place after a specific action. 334 | * 335 | * @param \Jackiedo\Cart\Action $action The specific action 336 | * 337 | * @return bool 338 | */ 339 | public function isBehindOf(Action $action) 340 | { 341 | return ($this->getHash() != $action->getHash()) && ($this->getOrderId() > $action->getOrderId()); 342 | } 343 | 344 | /** 345 | * Determines if this action is in the same group as the specific action. 346 | * 347 | * @param \Jackiedo\Cart\Action $action The specific action 348 | * 349 | * @return bool 350 | */ 351 | public function isSameGroupAs(Action $action) 352 | { 353 | return $this->getGroup() === $action->getGroup(); 354 | } 355 | 356 | /** 357 | * Get the amount of this action. 358 | * 359 | * @return float 360 | */ 361 | public function getAmount() 362 | { 363 | $rules = $this->getRules(); 364 | 365 | if (!$rules['enable'] || $this->isDeactivated()) { 366 | return 0; 367 | } 368 | 369 | // Prepare data 370 | $parentNode = $this->getParentNode(); 371 | $isPercentageValue = $this->isPercentage($this->attributes['value']); 372 | $value = floatval($this->attributes['value']); 373 | $target = $this->attributes['target']; 374 | $inclusiveAmount = $rules['include_calculations']; 375 | 376 | // Calculate target amount 377 | $targetAmount = floatval(('items_subtotal' === $target) ? $parentNode->getItemsSubtotal() : $parentNode->getTotalPrice()); 378 | $targetAmount = !is_null($inclusiveAmount) ? $targetAmount + $this->calcInclusiveAmount($inclusiveAmount) : $targetAmount; 379 | 380 | // Calculate action amount based on value and target amount 381 | if ($isPercentageValue) { 382 | $amount = $targetAmount * ($value / 100); 383 | $maxAmount = $rules['max_amount']; 384 | $minAmount = $rules['min_amount']; 385 | $isNegative = $amount < 0; 386 | 387 | if (!is_null($minAmount)) { 388 | $amount = $isNegative ? min($amount, $minAmount) : max($amount, $minAmount); 389 | } 390 | 391 | if (!is_null($maxAmount)) { 392 | $amount = $isNegative ? max($amount, $maxAmount) : min($amount, $maxAmount); 393 | } 394 | } else { 395 | $amount = ('price' === $target) ? $parentNode->getQuantity() * $value : $value; 396 | } 397 | 398 | return max(0 - $targetAmount, $amount); 399 | } 400 | 401 | /** 402 | * Calculates the inclusive amount corresponding to the target of the action. 403 | * 404 | * @param string $type The included type 405 | * 406 | * @return float 407 | */ 408 | protected function calcInclusiveAmount($type) 409 | { 410 | $container = $this->getCreator(); 411 | 412 | // If container is empty 413 | if ($container->isEmpty()) { 414 | return 0; 415 | } 416 | 417 | // If the included type is all previous groups 418 | if ('previous_groups' === $type) { 419 | return $container->sum(function ($action) { 420 | $isPreviousAction = $this->isBehindOf($action); 421 | $isAnotherGroup = !$this->isSameGroupAs($action); 422 | 423 | return ($isPreviousAction && $isAnotherGroup) ? $action->getAmount() : 0; 424 | }); 425 | } 426 | 427 | // If the included type is all previous actions 428 | if ('previous_actions' === $type) { 429 | return $container->sum(function ($action) { 430 | $isPreviousAction = $this->isBehindOf($action); 431 | 432 | return ($isPreviousAction) ? $action->getAmount() : 0; 433 | }); 434 | } 435 | 436 | // If the included type is all same group previous actions 437 | if ('same_group_previous_actions' === $type) { 438 | return $container->sum(function ($action) { 439 | $isPreviousAction = $this->isBehindOf($action); 440 | $isSameGroup = $this->isSameGroupAs($action); 441 | 442 | return ($isPreviousAction && $isSameGroup) ? $action->getAmount() : 0; 443 | }); 444 | } 445 | 446 | // If included type is an array of groups 447 | if (is_array($type)) { 448 | return $container->sum(function ($action) use ($type) { 449 | $isPreviousAction = $this->isBehindOf($action); 450 | $inAcceptedGroups = in_array($action->getGroup(), $type); 451 | 452 | return ($isPreviousAction && $inAcceptedGroups) ? $action->getAmount() : 0; 453 | }); 454 | } 455 | 456 | return 0; 457 | } 458 | 459 | /** 460 | * Initialize the attributes. 461 | * 462 | * @param array $attributes The action attributes 463 | * 464 | * @return $this 465 | */ 466 | protected function initAttributes(array $attributes = []) 467 | { 468 | // Add the missing default values ​​for the input 469 | $acceptedAttributes = array_keys($this->attributes); 470 | $attributes = array_merge($this->attributes, Arr::only($attributes, $acceptedAttributes)); 471 | 472 | // Validate the input 473 | $this->validate($attributes); 474 | 475 | // Stores the input into attributes 476 | $this->setAttributes($attributes); 477 | 478 | // Creates the sort number 479 | $this->orderNumber = intval(microtime(true) * 1000000); 480 | 481 | return $this; 482 | } 483 | 484 | /** 485 | * Set value for the attributes of this instance. 486 | * 487 | * @param array $attributes 488 | * 489 | * @return void 490 | */ 491 | protected function setAttributes(array $attributes = []) 492 | { 493 | foreach ($attributes as $attribute => $value) { 494 | switch ($attribute) { 495 | case 'target': 496 | $this->setTarget($value); 497 | break; 498 | 499 | case 'rules': 500 | $this->setRules($value); 501 | break; 502 | 503 | case 'extra_info': 504 | $this->setExtraInfo($value); 505 | break; 506 | 507 | default: 508 | $this->attributes[$attribute] = $value; 509 | break; 510 | } 511 | } 512 | } 513 | 514 | /** 515 | * Set value for the target attribute. 516 | * 517 | * @param string $value 518 | * 519 | * @return void 520 | */ 521 | protected function setTarget($value) 522 | { 523 | $parentNode = $this->getParentNode(); 524 | 525 | if ($parentNode instanceof Cart) { 526 | $this->attributes['target'] = 'items_subtotal'; 527 | } else { 528 | if (in_array($value, ['total_price', 'price'])) { 529 | $this->attributes['target'] = $value; 530 | } else { 531 | $this->attributes['target'] = 'total_price'; 532 | } 533 | } 534 | } 535 | 536 | /** 537 | * Set value for the rules attribute. 538 | * 539 | * @param mixed $value 540 | * 541 | * @return void 542 | */ 543 | protected function setRules($value) 544 | { 545 | $this->attributes['rules'] = is_array($value) ? array_merge($this->getRules(), $value) : $value; 546 | } 547 | 548 | /** 549 | * Determines whether the input is a percentage string. 550 | * 551 | * @param string $input 552 | * 553 | * @return bool 554 | */ 555 | protected function isPercentage($input) 556 | { 557 | if (!is_string($input)) { 558 | return false; 559 | } 560 | 561 | return '%' == substr($input, -1); 562 | } 563 | 564 | /** 565 | * Validates the input. 566 | * 567 | * @param array $attributes Array of input 568 | * 569 | * @return void 570 | * 571 | * @throws Jackiedo\Cart\Exceptions\InvalidArgumentException 572 | */ 573 | protected function validate(array $attributes = []) 574 | { 575 | if (array_key_exists('id', $attributes) && empty($attributes['id'])) { 576 | throw new InvalidArgumentException('The id attribute of the action is required.'); 577 | } 578 | 579 | if (array_key_exists('title', $attributes) && (!is_string($attributes['title']) || empty($attributes['title']))) { 580 | throw new InvalidArgumentException('The title attribute of the action is required.'); 581 | } 582 | 583 | if (array_key_exists('group', $attributes) && !is_string($attributes['group'])) { 584 | throw new InvalidArgumentException('The group attribute of the action must be a string.'); 585 | } 586 | 587 | if (array_key_exists('target', $attributes) && !is_null($attributes['target']) && !is_string($attributes['target'])) { 588 | throw new InvalidArgumentException('The target attribute of the action must be null or a string.'); 589 | } 590 | 591 | if (array_key_exists('value', $attributes) && !preg_match('/^\-{0,1}\d+(\.{0,1}\d+)?\%{0,1}$/', $attributes['value'])) { 592 | throw new InvalidArgumentException('The value attribute of the action must be a float numeric or percentage.'); 593 | } 594 | 595 | if (array_key_exists('rules', $attributes) && !is_array($attributes['rules']) && !($attributes['rules'] instanceof ActionHandler)) { 596 | throw new InvalidArgumentException('The rules attribute of the action must be an array or an instance of ' . ActionHandler::class . '.'); 597 | } 598 | 599 | if (array_key_exists('extra_info', $attributes) && !is_array($attributes['extra_info'])) { 600 | throw new InvalidArgumentException('The extra_info attribute of the action must be an array.'); 601 | } 602 | } 603 | } 604 | -------------------------------------------------------------------------------- /src/ActionsContainer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ActionsContainer extends Container 16 | { 17 | /** 18 | * The name of the accepted class is the creator. 19 | * 20 | * @var array 21 | */ 22 | protected $acceptedCreators = [ 23 | Cart::class, 24 | Item::class, 25 | ]; 26 | 27 | /** 28 | * Add an action into this container. 29 | * 30 | * @param array $attributes The action attributes 31 | * @param bool $withEvent Enable firing the event 32 | * 33 | * @return null|\Jackiedo\Cart\Action 34 | */ 35 | public function addAction(array $attributes = [], $withEvent = true) 36 | { 37 | $action = new Action($attributes); 38 | 39 | if ($withEvent) { 40 | $event = $this->fireEvent('cart.action.applying', [$action]); 41 | 42 | if (false === $event) { 43 | return null; 44 | } 45 | } 46 | 47 | $actionHash = $action->getHash(); 48 | 49 | if ($this->has($actionHash)) { 50 | // If the action is already exists in this container, we will update that action 51 | return $this->updateAction($actionHash, $attributes, $withEvent); 52 | } 53 | 54 | // If the action doesn't exist yet, put it to container 55 | $this->put($action->getHash(), $action); 56 | $this->sortActions(); 57 | 58 | if ($withEvent) { 59 | $this->fireEvent('cart.action.applied', [$action]); 60 | } 61 | 62 | return $action; 63 | } 64 | 65 | /** 66 | * Update an action in actions container. 67 | * 68 | * @param string $actionHash The unique identifier of action 69 | * @param array $attributes The new attributes 70 | * @param bool $withEvent Enable firing the event 71 | * 72 | * @return null|\Jackiedo\Cart\Action 73 | */ 74 | public function updateAction($actionHash, array $attributes = [], $withEvent = true) 75 | { 76 | $action = $this->getAction($actionHash); 77 | 78 | if ($withEvent) { 79 | $event = $this->fireEvent('cart.action.updating', [&$attributes, $action]); 80 | 81 | if (false === $event) { 82 | return null; 83 | } 84 | } 85 | 86 | $action->update($attributes); 87 | 88 | $newHash = $action->getHash(); 89 | 90 | if ($newHash != $actionHash) { 91 | $this->forget($actionHash); 92 | 93 | if ($this->has($newHash)) { 94 | $action = $this->updateAction($newHash, $attributes, $withEvent); 95 | } else { 96 | $this->put($newHash, $action); 97 | $this->sortActions(); 98 | } 99 | } 100 | 101 | if ($withEvent) { 102 | $this->fireEvent('cart.action.updated', [$action]); 103 | } 104 | 105 | return $action; 106 | } 107 | 108 | /** 109 | * Get an action in this container by given hash. 110 | * 111 | * @param string $actionHash The unique identifier of action 112 | * 113 | * @return \Jackiedo\Cart\Action 114 | */ 115 | public function getAction($actionHash) 116 | { 117 | if (!$this->has($actionHash)) { 118 | $this->throwInvalidHashException($actionHash); 119 | } 120 | 121 | return $this->get($actionHash); 122 | } 123 | 124 | /** 125 | * Get all actions in this container that match the given filter. 126 | * 127 | * @param mixed $filter Search filter 128 | * @param bool $complyAll indicates that the results returned must satisfy 129 | * all the conditions of the filter at the same time 130 | * or that only parts of the filter 131 | * 132 | * @return array 133 | */ 134 | public function getActions($filter = null, $complyAll = true) 135 | { 136 | // If there is no filter, return all taxes 137 | if (is_null($filter)) { 138 | return $this->all(); 139 | } 140 | 141 | // If filter is a closure 142 | if ($filter instanceof \Closure) { 143 | return $this->filter($filter)->all(); 144 | } 145 | 146 | // If filter is an array 147 | if (is_array($filter)) { 148 | // If filter is not an associative array 149 | if (!isAssocArray($filter)) { 150 | $filtered = $this->filter(function ($action) use ($filter) { 151 | return in_array($action->getHash(), $filter); 152 | }); 153 | 154 | return $filtered->all(); 155 | } 156 | 157 | // If filter is an associative 158 | if (!$complyAll) { 159 | $filtered = $this->filter(function ($action) use ($filter) { 160 | $intersects = array_intersect_assoc_recursive($action->getFilterValues(), $filter); 161 | 162 | return !empty($intersects); 163 | }); 164 | } else { 165 | $filtered = $this->filter(function ($action) use ($filter) { 166 | $diffs = array_diff_assoc_recursive($action->getFilterValues(), $filter); 167 | 168 | return empty($diffs); 169 | }); 170 | } 171 | 172 | return $filtered->all(); 173 | } 174 | 175 | return []; 176 | } 177 | 178 | /** 179 | * Remove an action instance from this container. 180 | * 181 | * @param string $actionHash The unique identifier of the action instance 182 | * @param bool $withEvent Enable firing the event 183 | * 184 | * @return $this 185 | */ 186 | public function removeAction($actionHash, $withEvent = true) 187 | { 188 | $action = $this->getAction($actionHash); 189 | 190 | if ($withEvent) { 191 | $event = $this->fireEvent('cart.action.removing', [$action]); 192 | 193 | if (false === $event) { 194 | return $this; 195 | } 196 | } 197 | 198 | $cart = $action->getCart(); 199 | $this->forget($actionHash); 200 | 201 | if ($withEvent) { 202 | $this->fireEvent('cart.action.removed', [$actionHash, clone $cart]); 203 | } 204 | 205 | return $this; 206 | } 207 | 208 | /** 209 | * Remove all action instances from this container. 210 | * 211 | * @param bool $withEvent Enable firing the event 212 | * 213 | * @return $this 214 | */ 215 | public function clearActions($withEvent = true) 216 | { 217 | $cart = $this->getCreator(); 218 | 219 | if ($cart instanceof Item) { 220 | $cart = $cart->getCart(); 221 | } 222 | 223 | if ($withEvent) { 224 | $event = $this->fireEvent('cart.action.clearing', [$cart]); 225 | 226 | if (false === $event) { 227 | return $this; 228 | } 229 | } 230 | 231 | $this->forgetAll(); 232 | 233 | if ($withEvent) { 234 | $this->fireEvent('cart.action.cleared', [$cart]); 235 | } 236 | 237 | return $this; 238 | } 239 | 240 | /** 241 | * Count all actions in this container that match the given filter. 242 | * 243 | * @param mixed $filter Search filter 244 | * @param bool $complyAll indicates that the results returned must satisfy 245 | * all the conditions of the filter at the same time 246 | * or that only parts of the filter 247 | * 248 | * @return int 249 | */ 250 | public function countActions($filter = null, $complyAll = true) 251 | { 252 | if ($this->isEmpty()) { 253 | return 0; 254 | } 255 | 256 | return count($this->getActions($filter, $complyAll)); 257 | } 258 | 259 | /** 260 | * Calculate the sum of action amounts in this container that match the given filter. 261 | * 262 | * @param mixed $filter Search filter 263 | * @param bool $complyAll indicates that the results returned must satisfy 264 | * all the conditions of the filter at the same time 265 | * or that only parts of the filter 266 | * 267 | * @return float 268 | */ 269 | public function sumAmount($filter = null, $complyAll = true) 270 | { 271 | if ($this->isEmpty()) { 272 | return 0; 273 | } 274 | 275 | $allActions = $this->getActions($filter, $complyAll); 276 | 277 | return array_reduce($allActions, function ($carry, $action) { 278 | return $carry + $action->getAmount(); 279 | }, 0); 280 | } 281 | 282 | /** 283 | * Sort all actions using the orderId attribute. 284 | * 285 | * @return $this 286 | */ 287 | protected function sortActions() 288 | { 289 | $sorted = $this->sortBy(function ($item) { 290 | return $item->getOrderId(); 291 | }); 292 | 293 | $this->items = $sorted->all(); 294 | 295 | return $this; 296 | } 297 | 298 | /** 299 | * Sort all actions using the orderId attribute with descending direction. 300 | * 301 | * @return $this 302 | */ 303 | protected function sortActionsDesc() 304 | { 305 | $sorted = $this->sortByDesc(function ($item) { 306 | return $item->getOrderId(); 307 | }); 308 | 309 | $this->items = $sorted->all(); 310 | 311 | return $this; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/Cart.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class Cart 21 | { 22 | use CanApplyAction; 23 | use FireEvent; 24 | 25 | /** 26 | * The root session name. 27 | * 28 | * @var string 29 | */ 30 | protected $rootSessionName; 31 | 32 | /** 33 | * The default cart name. 34 | * 35 | * @var string 36 | */ 37 | protected $defaultCartName = 'default'; 38 | 39 | /** 40 | * The name of current cart instance. 41 | * 42 | * @var string 43 | */ 44 | protected $cartName; 45 | 46 | /** 47 | * Create cart instance. 48 | * 49 | * @return void 50 | */ 51 | public function __construct() 52 | { 53 | $this->rootSessionName = '_' . md5(config('app.name') . __NAMESPACE__); 54 | $defaultCartName = config('cart.default_cart_name'); 55 | 56 | if (is_string($defaultCartName) && !empty($defaultCartName)) { 57 | $this->defaultCartName = $defaultCartName; 58 | } 59 | 60 | $this->name(); 61 | } 62 | 63 | /** 64 | * Select a cart to work with. 65 | * 66 | * @param null|string $name The cart name 67 | * 68 | * @return $this 69 | */ 70 | public function name($name = null) 71 | { 72 | $this->cartName = $this->rootSessionName . '.' . $this->standardizeCartName($name); 73 | 74 | $this->initSessions(); 75 | 76 | return $this; 77 | } 78 | 79 | /** 80 | * Create an another cart instance with the specific name. 81 | * 82 | * @param null|string $name The cart name 83 | * 84 | * @return $this 85 | */ 86 | public function newInstance($name = null) 87 | { 88 | $name = $this->standardizeCartName($name); 89 | 90 | if ($name === $this->getName()) { 91 | return clone $this; 92 | } 93 | 94 | $newInstance = new static; 95 | 96 | $newInstance->name($name); 97 | 98 | return $newInstance; 99 | } 100 | 101 | /** 102 | * Determines whether this cart has been grouped. 103 | * 104 | * @return bool 105 | */ 106 | public function hasBeenGrouped() 107 | { 108 | return Str::contains($this->getName(), ['.']); 109 | } 110 | 111 | /** 112 | * Determines whether this cart is in the specific group. 113 | * 114 | * @param string $groupName The specific group name 115 | * 116 | * @return bool 117 | */ 118 | public function isInGroup($groupName) 119 | { 120 | if (is_null($groupName)) { 121 | return false; 122 | } 123 | 124 | $currentGroupName = $this->getGroupName(); 125 | 126 | if (is_null($currentGroupName)) { 127 | return false; 128 | } 129 | 130 | return Str::startsWith($currentGroupName, $groupName); 131 | } 132 | 133 | /** 134 | * Get the group name of the cart. 135 | * 136 | * @return string 137 | */ 138 | public function getGroupName() 139 | { 140 | if (!$this->hasBeenGrouped()) { 141 | return null; 142 | } 143 | 144 | $splitParts = explode('.', $this->getName()); 145 | array_pop($splitParts); 146 | 147 | return implode('.', $splitParts); 148 | } 149 | 150 | /** 151 | * Get the current cart name. 152 | * 153 | * @return string 154 | */ 155 | public function getName() 156 | { 157 | return substr($this->cartName, strlen($this->rootSessionName) + 1); 158 | } 159 | 160 | /** 161 | * Get config of this cart. 162 | * 163 | * @param string $name The config name 164 | * @param mixed $default The return value if the config does not exist 165 | * 166 | * @return mixed 167 | */ 168 | public function getConfig($name = null, $default = null) 169 | { 170 | if ($name) { 171 | return session($this->getSessionPath('config.' . $name), $default); 172 | } 173 | 174 | return session($this->getSessionPath('config'), $default); 175 | } 176 | 177 | /** 178 | * Change whether the cart status is used for commercial or not. 179 | * 180 | * @param bool $status 181 | * 182 | * @return $this 183 | */ 184 | public function useForCommercial($status = true) 185 | { 186 | if ($this->isEmpty()) { 187 | $status = (bool) $status; 188 | 189 | $this->setConfig('use_for_commercial', $status); 190 | 191 | if ($status) { 192 | session()->put($this->getSessionPath('applied_actions'), new ActionsContainer); 193 | 194 | if ($this->getConfig('use_builtin_tax')) { 195 | session()->put($this->getSessionPath('applied_taxes'), new TaxesContainer); 196 | } else { 197 | session()->forget($this->getSessionPath('applied_taxes')); 198 | } 199 | } else { 200 | session()->forget($this->getSessionPath('applied_actions')); 201 | session()->forget($this->getSessionPath('applied_taxes')); 202 | } 203 | } 204 | 205 | return $this; 206 | } 207 | 208 | /** 209 | * Enable or disable built-in tax system for the cart. 210 | * This is only possible if the cart is empty. 211 | * 212 | * @param bool $status 213 | * 214 | * @return $this 215 | */ 216 | public function useBuiltinTax($status = true) 217 | { 218 | if ($this->isEmpty()) { 219 | $status = (bool) $status; 220 | 221 | $this->setConfig('use_builtin_tax', $status); 222 | 223 | if ($status && $this->getConfig('use_for_commercial', false)) { 224 | session()->put($this->getSessionPath('applied_taxes'), new TaxesContainer); 225 | } else { 226 | session()->forget($this->getSessionPath('applied_taxes')); 227 | } 228 | } 229 | 230 | return $this; 231 | } 232 | 233 | /** 234 | * Set default action rules for the cart. 235 | * This is only possible if the cart is empty. 236 | * 237 | * @param array $rules The default action rules 238 | */ 239 | public function setDefaultActionRules(array $rules = []) 240 | { 241 | if ($this->isEmpty()) { 242 | $this->setConfig('default_action_rules', $rules); 243 | } 244 | 245 | return $this; 246 | } 247 | 248 | /** 249 | * Set action groups order for the cart. 250 | * 251 | * @param array $order The action groups order 252 | * 253 | * @return $this 254 | */ 255 | public function setActionGroupsOrder(array $order = []) 256 | { 257 | $this->setConfig('action_groups_order', $order); 258 | 259 | return $this; 260 | } 261 | 262 | /** 263 | * Determines if the cart is empty. 264 | * 265 | * @return bool returns true if the cart has no items, no taxes, 266 | * and no action has been applied yet 267 | */ 268 | public function isEmpty() 269 | { 270 | return $this->hasNoItems() && $this->hasNoActions() && $this->hasNoTaxes(); 271 | } 272 | 273 | /** 274 | * Determines if the cart has no items. 275 | * 276 | * @return bool 277 | */ 278 | public function hasNoItems() 279 | { 280 | return $this->getItemsContainer()->isEmpty(); 281 | } 282 | 283 | /** 284 | * Determines if the cart has no actions. 285 | * 286 | * @return bool 287 | */ 288 | public function hasNoActions() 289 | { 290 | return $this->getActionsContainer()->isEmpty(); 291 | } 292 | 293 | /** 294 | * Determines if the cart has no taxes. 295 | * 296 | * @return bool 297 | */ 298 | public function hasNoTaxes() 299 | { 300 | return $this->getTaxesContainer()->isEmpty(); 301 | } 302 | 303 | /** 304 | * Determines if current cart is used for commcercial. 305 | * 306 | * @return bool 307 | */ 308 | public function isCommercialCart() 309 | { 310 | return $this->getConfig('use_for_commercial', false); 311 | } 312 | 313 | /** 314 | * Determines if current cart is enabled built-in tax system. 315 | * 316 | * @return bool 317 | */ 318 | public function isEnabledBuiltinTax() 319 | { 320 | if (!$this->getConfig('use_for_commercial', false)) { 321 | return false; 322 | } 323 | 324 | return $this->getConfig('use_builtin_tax', false); 325 | } 326 | 327 | /** 328 | * Remove cart from session. 329 | * 330 | * @param bool $withEvent Enable firing the event 331 | * 332 | * @return bool 333 | */ 334 | public function destroy($withEvent = true) 335 | { 336 | if ($withEvent) { 337 | $eventResponse = $this->fireEvent('cart.destroying', clone $this); 338 | 339 | if (false === $eventResponse) { 340 | return false; 341 | } 342 | } 343 | 344 | session()->forget($this->getSessionPath()); 345 | 346 | if ($withEvent) { 347 | $this->fireEvent('cart.destroyed'); 348 | } 349 | 350 | return true; 351 | } 352 | 353 | /** 354 | * Add an item into the items container. 355 | * 356 | * @param array $attributes The item attributes 357 | * @param bool $withEvent Enable firing the event 358 | * 359 | * @return null|\Jackiedo\Cart\Item 360 | */ 361 | public function addItem(array $attributes = [], $withEvent = true) 362 | { 363 | return $this->getItemsContainer()->addItem($attributes, $withEvent); 364 | } 365 | 366 | /** 367 | * Update an item in the items container. 368 | * 369 | * @param string $itemHash The unique identifier of the item 370 | * @param array|int $attributes New quantity of item or array of new attributes to update 371 | * @param bool $withEvent Enable firing the event 372 | * 373 | * @return null|\Jackiedo\Cart\Item 374 | */ 375 | public function updateItem($itemHash, $attributes = [], $withEvent = true) 376 | { 377 | return $this->getItemsContainer()->updateItem($itemHash, $attributes, $withEvent); 378 | } 379 | 380 | /** 381 | * Remove an item out of the items container. 382 | * 383 | * @param string $itemHash The unique identifier of the item 384 | * @param bool $withEvent Enable firing the event 385 | * 386 | * @return $this 387 | */ 388 | public function removeItem($itemHash, $withEvent = true) 389 | { 390 | $this->getItemsContainer()->removeItem($itemHash, $withEvent); 391 | 392 | return $this; 393 | } 394 | 395 | /** 396 | * Delete all items in the items container. 397 | * 398 | * @param bool $withEvent Enable firing the event 399 | * 400 | * @return $this 401 | */ 402 | public function clearItems($withEvent = true) 403 | { 404 | $this->getItemsContainer()->clearItems($withEvent); 405 | 406 | return $this; 407 | } 408 | 409 | /** 410 | * Get an item in the items container. 411 | * 412 | * @param string $itemHash The unique identifier of the item 413 | * 414 | * @return \Jackiedo\Cart\Item 415 | */ 416 | public function getItem($itemHash) 417 | { 418 | return $this->getItemsContainer()->getItem($itemHash); 419 | } 420 | 421 | /** 422 | * Get all items in the items container that match the given filter. 423 | * 424 | * @param mixed $filter Search filter 425 | * @param bool $complyAll indicates that the results returned must satisfy 426 | * all the conditions of the filter at the same time 427 | * or that only parts of the filter 428 | * 429 | * @return array 430 | */ 431 | public function getItems($filter = null, $complyAll = true) 432 | { 433 | return $this->getItemsContainer()->getItems($filter, $complyAll); 434 | } 435 | 436 | /** 437 | * Count the number of items in the items container that match the given filter. 438 | * 439 | * @param mixed $filter Search filter 440 | * @param bool $complyAll indicates that the results returned must satisfy 441 | * all the conditions of the filter at the same time 442 | * or that only parts of the filter 443 | * 444 | * @return int 445 | */ 446 | public function countItems($filter = null, $complyAll = true) 447 | { 448 | return $this->getItemsContainer()->countItems($filter, $complyAll); 449 | } 450 | 451 | /** 452 | * Sum the quantities of all items in the items container that match the given filter. 453 | * 454 | * @param mixed $filter Search filter 455 | * @param bool $complyAll indicates that the results returned must satisfy 456 | * all the conditions of the filter at the same time 457 | * or that only parts of the filter 458 | * 459 | * @return int 460 | */ 461 | public function sumItemsQuantity($filter = null, $complyAll = true) 462 | { 463 | return $this->getItemsContainer()->sumQuantity($filter, $complyAll); 464 | } 465 | 466 | /** 467 | * Determines if the item exists in the items container. 468 | * 469 | * @param string $itemHash The unique identifier of the item 470 | * 471 | * @return bool 472 | */ 473 | public function hasItem($itemHash) 474 | { 475 | return $this->getItemsContainer()->has($itemHash); 476 | } 477 | 478 | /** 479 | * Set value for one or some extended informations of the cart. 480 | * 481 | * @param array|string $information The information want to set 482 | * @param mixed $value The value of information 483 | * 484 | * @return $this 485 | */ 486 | public function setExtraInfo($information, $value = null) 487 | { 488 | return $this->setGroupExtraInfo($this->getName(), $information, $value); 489 | } 490 | 491 | /** 492 | * Get value of one or some extended informations of the cart 493 | * using "dot" notation. 494 | * 495 | * @param null|array|string $information The information want to get 496 | * @param mixed $default The return value if information does not exist 497 | * 498 | * @return mixed 499 | */ 500 | public function getExtraInfo($information = null, $default = null) 501 | { 502 | return $this->getGroupExtraInfo($this->getName(), $information, $default); 503 | } 504 | 505 | /** 506 | * Remove one or some extended informations of the cart 507 | * using "dot" notation. 508 | * 509 | * @param null|array|string $information The information want to remove 510 | * 511 | * @return $this 512 | */ 513 | public function removeExtraInfo($information = null) 514 | { 515 | return $this->removeGroupExtraInfo($this->getName(), $information); 516 | } 517 | 518 | /** 519 | * Set value for one or some extended informations of the group 520 | * using "dot" notation. 521 | * 522 | * @param string $groupName The name of the cart group 523 | * @param array|string $information The information want to set 524 | * @param mixed $value The value of information 525 | * 526 | * @return $this 527 | */ 528 | public function setGroupExtraInfo($groupName, $information, $value = null) 529 | { 530 | $groupName = trim($groupName, '.'); 531 | 532 | if ($groupName) { 533 | if (!is_array($information)) { 534 | $information = [ 535 | $information => $value, 536 | ]; 537 | } 538 | 539 | foreach ($information as $key => $value) { 540 | $key = trim($key, '.'); 541 | 542 | if (!empty($key)) { 543 | session()->put($this->rootSessionName . '.' . $groupName . '.extra_info.' . $key, $value); 544 | } 545 | } 546 | } 547 | 548 | return $this; 549 | } 550 | 551 | /** 552 | * Get value of one or some extended informations of the group 553 | * using "dot" notation. 554 | * 555 | * @param string $groupName The name of the cart group 556 | * @param null|array|string $information The information want to get 557 | * @param mixed $default The return value if information does not exist 558 | * 559 | * @return mixed 560 | */ 561 | public function getGroupExtraInfo($groupName, $information = null, $default = null) 562 | { 563 | $groupName = trim($groupName, '.'); 564 | 565 | if ($groupName) { 566 | $extraInfo = session($this->rootSessionName . '.' . $groupName . '.extra_info', []); 567 | 568 | if (is_null($information)) { 569 | return $extraInfo; 570 | } 571 | 572 | if (is_array($information)) { 573 | return Arr::only($extraInfo, $information); 574 | } 575 | 576 | return Arr::get($extraInfo, $information, $default); 577 | } 578 | 579 | return $default; 580 | } 581 | 582 | /** 583 | * Remove one or some extended informations of the group 584 | * using "dot" notation. 585 | * 586 | * @param string $groupName The name of the cart group 587 | * @param null|array|string $information The information want to remove 588 | * 589 | * @return $this 590 | */ 591 | public function removeGroupExtraInfo($groupName, $information = null) 592 | { 593 | $groupName = trim($groupName, '.'); 594 | 595 | if ($groupName) { 596 | if (is_null($information)) { 597 | session()->put($this->rootSessionName . '.' . $groupName . '.extra_info', []); 598 | 599 | return $this; 600 | } 601 | 602 | $informations = (array) $information; 603 | 604 | foreach ($informations as $key) { 605 | $key = trim($key, '.'); 606 | 607 | if (!empty($key)) { 608 | session()->forget($this->rootSessionName . '.' . $groupName . '.extra_info.' . $key); 609 | } 610 | } 611 | } 612 | 613 | return $this; 614 | } 615 | 616 | /** 617 | * Add a tax into the taxes container of this cart. 618 | * 619 | * @param array $attributes The tax attributes 620 | * @param bool $withEvent Enable firing the event 621 | * 622 | * @return null|\Jackiedo\Cart\Tax 623 | */ 624 | public function applyTax(array $attributes = [], $withEvent = true) 625 | { 626 | if (!$this->isEnabledBuiltinTax()) { 627 | return null; 628 | } 629 | 630 | return $this->getTaxesContainer()->addTax($attributes, $withEvent); 631 | } 632 | 633 | /** 634 | * Update a tax in the taxes container. 635 | * 636 | * @param string $taxHash The unique identifire of the tax instance 637 | * @param array $attributes The new attributes 638 | * @param bool $withEvent Enable firing the event 639 | * 640 | * @return null|\Jackiedo\Cart\Tax 641 | */ 642 | public function updateTax($taxHash, array $attributes = [], $withEvent = true) 643 | { 644 | return $this->getTaxesContainer()->updateTax($taxHash, $attributes, $withEvent); 645 | } 646 | 647 | /** 648 | * Get an applied tax in the taxes container of this cart. 649 | * 650 | * @param string $taxHash The unique identifire of the tax instance 651 | * 652 | * @return \Jackiedo\Cart\Tax 653 | */ 654 | public function getTax($taxHash) 655 | { 656 | return $this->getTaxesContainer()->getTax($taxHash); 657 | } 658 | 659 | /** 660 | * Get all tax instances in the taxes container of this cart that match the given filter. 661 | * 662 | * @param mixed $filter Search filter 663 | * @param bool $complyAll indicates that the results returned must satisfy 664 | * all the conditions of the filter at the same time 665 | * or that only parts of the filter 666 | * 667 | * @return array 668 | */ 669 | public function getTaxes($filter = null, $complyAll = true) 670 | { 671 | return $this->getTaxesContainer()->getTaxes($filter, $complyAll); 672 | } 673 | 674 | /** 675 | * Count all taxes in the actions container that match the given filter. 676 | * 677 | * @param mixed $filter Search filter 678 | * @param bool $complyAll indicates that the results returned must satisfy 679 | * all the conditions of the filter at the same time 680 | * or that only parts of the filter 681 | * 682 | * @return int 683 | */ 684 | public function countTaxes($filter = null, $complyAll = true) 685 | { 686 | return $this->getTaxesContainer()->countTaxes($filter, $complyAll); 687 | } 688 | 689 | /** 690 | * Determines if the tax exists in the taxes container of this cart. 691 | * 692 | * @param string $taxHash The unique identifier of the tax 693 | * 694 | * @return bool 695 | */ 696 | public function hasTax($taxHash) 697 | { 698 | return $this->getTaxesContainer()->has($taxHash); 699 | } 700 | 701 | /** 702 | * Remove an applied tax from the taxes container of this cart. 703 | * 704 | * @param string $taxHash The unique identifier of the tax instance 705 | * @param bool $withEvent Enable firing the event 706 | * 707 | * @return $this 708 | */ 709 | public function removeTax($taxHash, $withEvent = true) 710 | { 711 | $this->getTaxesContainer()->removeTax($taxHash, $withEvent); 712 | 713 | return $this; 714 | } 715 | 716 | /** 717 | * Remove all apllied taxes from the taxes container of this cart. 718 | * 719 | * @param bool $withEvent Enable firing the event 720 | * 721 | * @return $this 722 | */ 723 | public function clearTaxes($withEvent = true) 724 | { 725 | $this->getTaxesContainer()->clearTaxes($withEvent); 726 | 727 | return $this; 728 | } 729 | 730 | /** 731 | * Get the subtotal amount of all items in the items container. 732 | * 733 | * @return float 734 | */ 735 | public function getItemsSubtotal() 736 | { 737 | return $this->getItemsContainer()->sumSubtotal(); 738 | } 739 | 740 | /** 741 | * Get the sum amount of all items subtotal amount and all actions amount. 742 | * 743 | * @return float 744 | */ 745 | public function getSubtotal() 746 | { 747 | $enabledActionsAmount = $this->getActionsContainer()->sumAmount(); 748 | 749 | return $this->getItemsSubtotal() + $enabledActionsAmount; 750 | } 751 | 752 | /** 753 | * Calculate total taxable amounts include the taxable amount of cart and all items. 754 | * 755 | * @return float 756 | */ 757 | public function getTaxableAmount() 758 | { 759 | if (!$this->isEnabledBuiltinTax()) { 760 | return 0; 761 | } 762 | 763 | $itemsTaxableAmount = $this->getItemsContainer()->sumTaxableAmount(); 764 | $cartTaxableAmount = $this->getActionsContainer()->sumAmount([ 765 | 'rules' => [ 766 | 'taxable' => true, 767 | ], 768 | ]); 769 | 770 | return $itemsTaxableAmount + $cartTaxableAmount; 771 | } 772 | 773 | /** 774 | * Get the total tax rate applied to the current cart. 775 | * 776 | * @return float 777 | */ 778 | public function getTaxRate() 779 | { 780 | if (!$this->isEnabledBuiltinTax()) { 781 | return 0; 782 | } 783 | 784 | return $this->getTaxesContainer()->sumRate(); 785 | } 786 | 787 | /** 788 | * Get the total tax amount applied to the current cart. 789 | * 790 | * @return float 791 | */ 792 | public function getTaxAmount() 793 | { 794 | if (!$this->isEnabledBuiltinTax()) { 795 | return 0; 796 | } 797 | 798 | return $this->getTaxesContainer()->sumAmount(); 799 | } 800 | 801 | /** 802 | * Get the total amount of the current cart. 803 | * 804 | * @return float 805 | */ 806 | public function getTotal() 807 | { 808 | return $this->getSubtotal() + $this->getTaxAmount(); 809 | } 810 | 811 | /** 812 | * Get all information of cart as a collection. 813 | * 814 | * @param bool $withItems Include details of added items in the result 815 | * @param bool $withActions Include details of applied actions in the result 816 | * @param bool $withTaxes Include details of applied taxes in the result 817 | * 818 | * @return \Jackiedo\Cart\Details 819 | */ 820 | public function getDetails($withItems = true, $withActions = true, $withTaxes = true) 821 | { 822 | $details = new Details; 823 | $isCommercialCart = $this->isCommercialCart(); 824 | $enabledBuiltinTax = $this->isEnabledBuiltinTax(); 825 | $itemsContainer = $this->getItemsContainer(); 826 | 827 | $details->put('type', 'cart'); 828 | $details->put('name', $this->getName()); 829 | $details->put('commercial_cart', $isCommercialCart); 830 | $details->put('enabled_builtin_tax', $enabledBuiltinTax); 831 | $details->put('items_count', $this->countItems()); 832 | $details->put('quantities_sum', $this->sumItemsQuantity()); 833 | 834 | if ($isCommercialCart) { 835 | $actionsContainer = $this->getActionsContainer(); 836 | 837 | $details->put('items_subtotal', $this->getItemsSubtotal()); 838 | $details->put('actions_count', $this->countActions()); 839 | $details->put('actions_amount', $this->sumActionsAmount()); 840 | 841 | if ($enabledBuiltinTax) { 842 | $taxesContainer = $this->getTaxesContainer(); 843 | 844 | $details->put('subtotal', $this->getSubtotal()); 845 | $details->put('taxes_count', $this->countTaxes()); 846 | $details->put('taxable_amount', $this->getTaxableAmount()); 847 | $details->put('tax_rate', $this->getTaxRate()); 848 | $details->put('tax_amount', $this->getTaxAmount()); 849 | $details->put('total', $this->getTotal()); 850 | 851 | if ($withItems) { 852 | $details->put('items', $itemsContainer->getDetails($withActions)); 853 | } 854 | 855 | if ($withActions) { 856 | $details->put('applied_actions', $actionsContainer->getDetails()); 857 | } 858 | 859 | if ($withTaxes) { 860 | $details->put('applied_taxes', $taxesContainer->getDetails()); 861 | } 862 | } else { 863 | $details->put('total', $this->getSubtotal()); 864 | 865 | if ($withItems) { 866 | $details->put('items', $itemsContainer->getDetails($withActions)); 867 | } 868 | 869 | if ($withActions) { 870 | $details->put('applied_actions', $actionsContainer->getDetails()); 871 | } 872 | } 873 | } else { 874 | if ($withItems) { 875 | $details->put('items', $itemsContainer->getDetails($withActions)); 876 | } 877 | } 878 | 879 | $details->put('extra_info', new Details($this->getExtraInfo(null, []))); 880 | 881 | return $details; 882 | } 883 | 884 | /** 885 | * Get all information of cart group as a collection. 886 | * 887 | * @param null|string $groupName The group part from cart name 888 | * @param bool $withCartsHaveNoItems Include carts have no items in the result 889 | * @param bool $withItems Include details of added items in the result 890 | * @param bool $withActions Include details of applied actions in the result 891 | * @param bool $withTaxes Include details of applied taxes in the result 892 | * 893 | * @return \Jackiedo\Cart\Details 894 | */ 895 | public function getGroupDetails($groupName = null, $withCartsHaveNoItems = false, $withItems = true, $withActions = true, $withTaxes = true) 896 | { 897 | $groupName = $groupName ?: $this->getGroupName(); 898 | 899 | return $this->groupAnalysic($groupName, $withCartsHaveNoItems, $withItems, $withActions, $withTaxes); 900 | } 901 | 902 | /** 903 | * Standardize the cart name. 904 | * 905 | * @param null|string $name The cart name before standardized 906 | * 907 | * @return string 908 | * 909 | * @throws \Jackiedo\Cart\Exceptions\InvalidCartNameException 910 | */ 911 | protected function standardizeCartName($name = null) 912 | { 913 | $name = $name ?: $this->defaultCartName; 914 | $name = trim($name, '.'); 915 | 916 | if (in_array('extra_info', explode('.', $name))) { 917 | throw new InvalidCartNameException("The keyword 'extra_info' is not allowed to name the cart or group."); 918 | } 919 | 920 | return $name; 921 | } 922 | 923 | /** 924 | * Initialize attributes for current cart instance. 925 | * 926 | * @return bool return false if attributes already exist without initialization 927 | */ 928 | protected function initSessions() 929 | { 930 | if (!session()->has($this->getSessionPath())) { 931 | $appConfig = config('cart'); 932 | $noneCommercialCarts = array_values((array) Arr::get($appConfig, 'none_commercial_carts', [])); 933 | $useForCommercial = !in_array($this->getName(), $noneCommercialCarts); 934 | $useBuiltinTax = (bool) Arr::get($appConfig, 'use_builtin_tax', false); 935 | 936 | $this->setConfig('use_for_commercial', $useForCommercial); 937 | $this->setConfig('use_builtin_tax', $useBuiltinTax); 938 | $this->setConfig('default_tax_rate', floatval(Arr::get($appConfig, 'default_tax_rate', 0))); 939 | $this->setConfig('default_action_rules', (array) Arr::get($appConfig, 'default_action_rules', [])); 940 | $this->setConfig('action_groups_order', array_values((array) Arr::get($appConfig, 'action_groups_order', []))); 941 | 942 | session()->put($this->getSessionPath('type'), 'cart'); 943 | session()->put($this->getSessionPath('name'), $this->getName()); 944 | session()->put($this->getSessionPath('extra_info'), []); 945 | session()->put($this->getSessionPath('items'), new ItemsContainer); 946 | 947 | if ($useForCommercial) { 948 | session()->put($this->getSessionPath('applied_actions'), new ActionsContainer); 949 | 950 | if ($useBuiltinTax) { 951 | session()->put($this->getSessionPath('applied_taxes'), new TaxesContainer); 952 | } 953 | } 954 | 955 | return true; 956 | } 957 | 958 | return false; 959 | } 960 | 961 | /** 962 | * Set config for this cart. 963 | * 964 | * @param string $name 965 | * @param mixed $value 966 | * 967 | * @return $this 968 | */ 969 | protected function setConfig($name, $value = null) 970 | { 971 | if ($name) { 972 | session()->put($this->getSessionPath('config.' . $name), $value); 973 | } 974 | 975 | return $this; 976 | } 977 | 978 | /** 979 | * Get the session path from the path to the cart. 980 | * 981 | * @param mixed $sessionKey 982 | * 983 | * @return string $sessionKey The sub session key from session of this cart 984 | */ 985 | protected function getSessionPath($sessionKey = null) 986 | { 987 | if (is_null($sessionKey)) { 988 | return $this->cartName; 989 | } 990 | 991 | return $this->cartName . '.' . $sessionKey; 992 | } 993 | 994 | /** 995 | * Get the items container. 996 | * 997 | * @return \Jackiedo\Cart\ItemsContainer 998 | */ 999 | protected function getItemsContainer() 1000 | { 1001 | return session($this->getSessionPath('items'), new ItemsContainer); 1002 | } 1003 | 1004 | /** 1005 | * Get the taxes container. 1006 | * 1007 | * @return \Jackiedo\Cart\TaxesContainer 1008 | */ 1009 | protected function getTaxesContainer() 1010 | { 1011 | return session($this->getSessionPath('applied_taxes'), new TaxesContainer); 1012 | } 1013 | 1014 | /** 1015 | * Get the actions container. 1016 | * 1017 | * @return \Jackiedo\Cart\ActionsContainer 1018 | */ 1019 | protected function getActionsContainer() 1020 | { 1021 | return session($this->getSessionPath('applied_actions'), new ActionsContainer); 1022 | } 1023 | 1024 | /** 1025 | * Indicates whether this instance can apply cart. 1026 | * 1027 | * @return bool 1028 | */ 1029 | protected function canApplyAction() 1030 | { 1031 | if ($this->isCommercialCart()) { 1032 | return true; 1033 | } 1034 | 1035 | return false; 1036 | } 1037 | 1038 | /** 1039 | * Analyze data from the session group. 1040 | * 1041 | * @param string $groupName The group part from cart name 1042 | * @param bool $withCartsHaveNoItems Include carts have no items in the result 1043 | * @param bool $withItems Include details of added items in the result 1044 | * @param bool $withActions Include details of applied actions in the result 1045 | * @param bool $withTaxes Include details of applied taxes in the result 1046 | * @param array $moneyAmount Information on cumulative amounts from the details of the subsections 1047 | * @param array $moneyAmounts 1048 | * 1049 | * @return \Jackiedo\Cart\Details 1050 | */ 1051 | protected function groupAnalysic($groupName, $withCartsHaveNoItems, $withItems, $withActions, $withTaxes, array $moneyAmounts = []) 1052 | { 1053 | $info = session($this->rootSessionName . '.' . $groupName, []); 1054 | 1055 | // If this is a group 1056 | if ('cart' !== Arr::get($info, 'type')) { 1057 | $extraInfo = Arr::get($info, 'extra_info', []); 1058 | $info = Arr::except($info, ['extra_info']); 1059 | $details = new Details; 1060 | $subsections = new Details; 1061 | 1062 | $details->put('type', 'group'); 1063 | $details->put('name', $groupName); 1064 | 1065 | foreach ($info as $key => $value) { 1066 | // Get details of subsections 1067 | $subInfo = $this->groupAnalysic($groupName . '.' . $key, $withCartsHaveNoItems, $withItems, $withActions, $withTaxes, $moneyAmounts); 1068 | 1069 | if ($subInfo instanceof Details) { 1070 | if ($subInfo->has(['subtotal', 'tax_amount'])) { 1071 | $moneyAmounts['subtotal'] = Arr::get($moneyAmounts, 'subtotal', 0) + $subInfo->get('subtotal', 0); 1072 | $moneyAmounts['tax_amount'] = Arr::get($moneyAmounts, 'tax_amount', 0) + $subInfo->get('tax_amount', 0); 1073 | } 1074 | 1075 | if ($subInfo->has(['total'])) { 1076 | $moneyAmounts['total'] = Arr::get($moneyAmounts, 'total', 0) + $subInfo->get('total', 0); 1077 | } 1078 | 1079 | $subsections->put($key, $subInfo); 1080 | } 1081 | } 1082 | 1083 | $details->put('items_count', $subsections->sum('items_count')); 1084 | $details->put('quantities_sum', $subsections->sum(function ($section) { 1085 | return $section->get('quantities_sum', $section->get('items_count')); 1086 | })); 1087 | 1088 | if (!empty($moneyAmounts)) { 1089 | foreach ($moneyAmounts as $key => $value) { 1090 | $details->put($key, $value); 1091 | } 1092 | } 1093 | 1094 | $details->put('subsections', $subsections); 1095 | $details->put('extra_info', $extraInfo); 1096 | 1097 | // Return group details 1098 | return $details; 1099 | } 1100 | 1101 | // If this is a cart 1102 | $cart = $this->newInstance($groupName); 1103 | 1104 | if (!$withCartsHaveNoItems && $cart->hasNoItems()) { 1105 | return null; 1106 | } 1107 | 1108 | return $cart->getDetails($withItems, $withActions, $withTaxes); 1109 | } 1110 | } 1111 | -------------------------------------------------------------------------------- /src/CartServiceProvider.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class CartServiceProvider extends ServiceProvider 15 | { 16 | /** 17 | * Indicates if loading of the provider is deferred. 18 | * 19 | * @var bool 20 | */ 21 | protected $defer = false; 22 | 23 | /** 24 | * Bootstrap the application events. 25 | * 26 | * @return void 27 | */ 28 | public function boot() 29 | { 30 | $packageConfigPath = __DIR__ . '/Config/config.php'; 31 | $appConfigPath = config_path('cart.php'); 32 | 33 | $this->mergeConfigFrom($packageConfigPath, 'cart'); 34 | 35 | $this->publishes([ 36 | $packageConfigPath => $appConfigPath, 37 | ], 'config'); 38 | } 39 | 40 | /** 41 | * Register the service provider. 42 | * 43 | * @return void 44 | */ 45 | public function register() 46 | { 47 | $this->app->bind('cart', 'Jackiedo\Cart\Cart'); 48 | } 49 | 50 | /** 51 | * Get the services provided by the provider. 52 | * 53 | * @return array 54 | */ 55 | public function provides() 56 | { 57 | return [ 58 | 'cart', 59 | ]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Config/config.php: -------------------------------------------------------------------------------- 1 | 'default', 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | None commercial carts 18 | |-------------------------------------------------------------------------- 19 | | 20 | | This setting allows you to specify which carts (by the name) are not for 21 | | commercial use but used for other purposes such as storing recent viewed 22 | | items, compared items... They are have no information regarding money. 23 | | 24 | */ 25 | 'none_commercial_carts' => [ 26 | // 'example_cart_name' 27 | ], 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Use built-in tax system 32 | |-------------------------------------------------------------------------- 33 | | 34 | | This setting allows you to set the default state of using the built-in 35 | | taxing system or not every time you initialize the cart. 36 | | 37 | */ 38 | 'use_builtin_tax' => true, 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Default tax rate 43 | |-------------------------------------------------------------------------- 44 | | 45 | | This is the default tax rate value used for each tax of the cart if you do 46 | | not set the rate attribute every time apply a tax into cart. 47 | | 48 | */ 49 | 'default_tax_rate' => 10, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Default rules of actions 54 | |-------------------------------------------------------------------------- 55 | | 56 | | This is the default rules attribute of action when you apply an action 57 | | into cart or item. The value of this setting shows how to calculate the 58 | | amount for the action. For details about the available keys and values ​​of 59 | | this setting, please see https://github.com/JackieDo/Laravel-Cart 60 | | 61 | */ 62 | 'default_action_rules' => [ 63 | 'enable' => true, 64 | 'taxable' => true, 65 | 'allow_others_disable' => true, 66 | 'disable_others' => null, 67 | 'include_calculations' => 'same_group_previous_actions', 68 | 'max_amount' => null, 69 | 'min_amount' => null, 70 | ], 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Action groups order 75 | |-------------------------------------------------------------------------- 76 | | 77 | | This setting allows to prioritize groups of actions in descending order. 78 | | If not set, actions will be sorted according to the time of applied. In 79 | | contrast, the actions will be sorted by groups order first, then sorted 80 | | by the time of applied. 81 | | 82 | */ 83 | 'action_groups_order' => [ 84 | // 'example_action_group_name' 85 | ], 86 | ]; 87 | -------------------------------------------------------------------------------- /src/Container.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class Container extends Collection 19 | { 20 | use CollectionForgetAll; 21 | use BackToCreator; 22 | use FireEvent; 23 | 24 | /** 25 | * Create a new container. 26 | * 27 | * @param mixed $items 28 | * 29 | * @return void 30 | */ 31 | public function __construct($items = []) 32 | { 33 | $this->storeCreator(); 34 | 35 | parent::__construct($items); 36 | } 37 | 38 | /** 39 | * Get details information of this container as a collection. 40 | * 41 | * @return \Jackiedo\Cart\Details 42 | */ 43 | public function getDetails() 44 | { 45 | $details = new Details; 46 | $allActions = $this->all(); 47 | 48 | foreach ($allActions as $key => $value) { 49 | $details->put($key, $value->getDetails()); 50 | } 51 | 52 | return $details; 53 | } 54 | 55 | /** 56 | * Check for the existence of the hash string. 57 | * 58 | * @param string $hash The hash string 59 | * 60 | * @return void 61 | * 62 | * @throws \Jackiedo\Cart\Exceptions\InvalidHashException 63 | */ 64 | protected function throwInvalidHashException($hash) 65 | { 66 | throw new InvalidHashException('Could not find any action with hash ' . $hash . ' in the actions container.'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Contracts/ActionHandler.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface CartNode 13 | { 14 | /** 15 | * Check if the parent node can be found. 16 | * 17 | * @return bool 18 | */ 19 | public function hasKnownParentNode(); 20 | 21 | /** 22 | * Get parent node instance that this instance is belong to. 23 | * 24 | * @return object 25 | */ 26 | public function getParentNode(); 27 | 28 | /** 29 | * Get the cart instance that this node belong to. 30 | * 31 | * @return \Jackiedo\Cart\Cart 32 | */ 33 | public function getCart(); 34 | 35 | /** 36 | * Determines which values ​​to filter. 37 | * 38 | * @return array 39 | */ 40 | public function getFilterValues(); 41 | 42 | /** 43 | * Get config of the cart instance thet this node belong to. 44 | * 45 | * @param null|string $name The config name 46 | * @param mixed $default The return value if the config does not exist 47 | * 48 | * @return mixed 49 | */ 50 | public function getConfig($name = null, $default = null); 51 | 52 | /** 53 | * Get the cart node's original attribute values. 54 | * 55 | * @param null|string $attribute The attribute 56 | * @param mixed $default The return value if attribute does not exist 57 | * 58 | * @return mixed 59 | */ 60 | public function getOriginal($attribute = null, $default = null); 61 | 62 | /** 63 | * Dynamic attribute getter. 64 | * 65 | * @param string $attribute The attribute 66 | * @param mixed $default The return value if attribute does not exist 67 | * 68 | * @return mixed 69 | */ 70 | public function get($attribute, $default = null); 71 | } 72 | -------------------------------------------------------------------------------- /src/Contracts/UseCartable.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | interface UseCartable 13 | { 14 | /** 15 | * Get the identifier of the UseCartable item. 16 | * 17 | * @return int|string 18 | */ 19 | public function getUseCartableId(); 20 | 21 | /** 22 | * Get the title of the UseCartable item. 23 | * 24 | * @return string 25 | */ 26 | public function getUseCartableTitle(); 27 | 28 | /** 29 | * Get the price of the UseCartable item. 30 | * 31 | * @return float 32 | */ 33 | public function getUseCartablePrice(); 34 | 35 | /** 36 | * Add the UseCartable item to the cart. 37 | * 38 | * @param \Jackiedo\Cart\Cart|string $cartOrName The cart instance or the name of the cart 39 | * @param array $attributes The additional attributes 40 | * @param bool $withEvent Enable firing the event 41 | * 42 | * @return null|\Jackiedo\Cart\Item 43 | */ 44 | public function addToCart($cartOrName, array $attributes = [], $withEvent = true); 45 | 46 | /** 47 | * Determines the UseCartable item has in the cart. 48 | * 49 | * @param \Jackiedo\Cart\Cart|string $cartOrName The cart instance or the name of the cart 50 | * @param array $filter Array of additional filter 51 | * 52 | * @return bool 53 | */ 54 | public function hasInCart($cartOrName, array $filter = []); 55 | 56 | /** 57 | * Get all the UseCartable item in the cart. 58 | * 59 | * @param \Jackiedo\Cart\Cart|string $cartOrName The cart instance or the name of the cart 60 | * 61 | * @return array 62 | */ 63 | public function allFromCart($cartOrName); 64 | 65 | /** 66 | * Get the UseCartable items in the cart with given additional filter. 67 | * 68 | * @param \Jackiedo\Cart\Cart|string $cartOrName The cart instance or the name of the cart 69 | * @param array $filter Array of additional filter 70 | * 71 | * @return array 72 | */ 73 | public function searchInCart($cartOrName, array $filter = []); 74 | 75 | /** 76 | * Find a model by its identifier. 77 | * 78 | * @param int $id The identifier of model 79 | * 80 | * @return null|\Illuminate\Support\Collection|static 81 | */ 82 | public function findById($id); 83 | } 84 | -------------------------------------------------------------------------------- /src/Details.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class Details extends Collection 17 | { 18 | /** 19 | * Dynamically access item from collection. 20 | * 21 | * @param string $key 22 | * 23 | * @return mixed 24 | */ 25 | public function __get($key) 26 | { 27 | if (class_exists('\Illuminate\Support\HigherOrderCollectionProxy') && in_array($key, static::$proxies)) { 28 | return new \Illuminate\Support\HigherOrderCollectionProxy($this, $key); 29 | } 30 | 31 | return $this->get($key); 32 | } 33 | 34 | /** 35 | * Determine if an item exists in the collection by key. 36 | * 37 | * @param mixed $key 38 | * 39 | * @return bool 40 | */ 41 | public function has($key) 42 | { 43 | $keys = is_array($key) ? $key : func_get_args(); 44 | 45 | foreach ($keys as $value) { 46 | if (!array_key_exists($value, $this->items)) { 47 | return false; 48 | } 49 | } 50 | 51 | return true; 52 | } 53 | 54 | /** 55 | * Get an item from the collection by key. 56 | * 57 | * @param mixed $key 58 | * @param mixed $default 59 | * 60 | * @return mixed 61 | */ 62 | public function get($key, $default = null) 63 | { 64 | if ('model' === $key) { 65 | if ($this->has(['id', 'associated_class'])) { 66 | $id = $this->get('id'); 67 | $associatedClass = $this->get('associated_class'); 68 | 69 | if (!class_exists($associatedClass)) { 70 | throw new InvalidAssociatedException('The [' . $associatedClass . '] class does not exist.'); 71 | } 72 | 73 | $model = with(new $associatedClass)->findById($id); 74 | 75 | if (!$model) { 76 | throw new InvalidModelException('The supplied associated model from [' . $associatedClass . '] does not exist.'); 77 | } 78 | 79 | return $model; 80 | } 81 | 82 | return $default; 83 | } 84 | 85 | return parent::get($key, $default); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InvalidArgumentException extends \Exception 13 | { 14 | // 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidAssociatedException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InvalidAssociatedException extends \Exception 13 | { 14 | // 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidCartNameException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InvalidCartNameException extends \Exception 13 | { 14 | // 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidHashException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InvalidHashException extends \Exception 13 | { 14 | // 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidModelException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class InvalidModelException extends \Exception 13 | { 14 | // 15 | } 16 | -------------------------------------------------------------------------------- /src/Exceptions/UnknownCreatorException.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UnknownCreatorException extends \Exception 13 | { 14 | // 15 | } 16 | -------------------------------------------------------------------------------- /src/Facades/Cart.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Cart extends Facade 15 | { 16 | /** 17 | * Get the registered name of the component. 18 | * 19 | * @return string 20 | */ 21 | protected static function getFacadeAccessor() 22 | { 23 | return 'cart'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Helpers/helpers.php: -------------------------------------------------------------------------------- 1 | $value) { 178 | if (is_array($array[$key])) { 179 | $sortValue = ksort_recursive($array[$key], $sort_flags); 180 | 181 | if (!$sortValue) { 182 | return false; 183 | } 184 | } 185 | } 186 | 187 | return true; 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/Item.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class Item implements CartNode 22 | { 23 | use CanBeCartNode; 24 | use CanApplyAction; 25 | 26 | /** 27 | * The attributes of item. 28 | * 29 | * @var array 30 | */ 31 | protected $attributes = [ 32 | 'associated_class' => null, 33 | 'id' => null, 34 | 'title' => null, 35 | 'quantity' => 1, 36 | 'price' => 0, 37 | 'taxable' => true, 38 | 'options' => [], 39 | 'extra_info' => [], 40 | ]; 41 | 42 | /** 43 | * The name of the accepted class is the creator. 44 | * 45 | * @var array 46 | */ 47 | protected $acceptedCreators = [ 48 | ItemsContainer::class, 49 | ]; 50 | 51 | /** 52 | * Stores applied actions. 53 | * 54 | * @var \Illuminate\Support\Collection 55 | */ 56 | protected $appliedActions; 57 | 58 | /** 59 | * Indicates whether or not this item belong to a commercial cart. 60 | * 61 | * @var bool 62 | */ 63 | protected $inCommercialCart = false; 64 | 65 | /** 66 | * Indicates whether or not this item belongs to a taxable cart. 67 | * 68 | * @var bool 69 | */ 70 | protected $enabledBuiltinTax = false; 71 | 72 | /** 73 | * The constructor. 74 | * 75 | * @param array $attributes The item attributes 76 | * 77 | * @return void 78 | */ 79 | public function __construct(array $attributes = []) 80 | { 81 | // Store the creator 82 | $this->storeCreator(0, function ($creator, $caller) { 83 | $cart = $creator->getCreator(); 84 | $this->inCommercialCart = $cart->isCommercialCart(); 85 | $this->enabledBuiltinTax = $cart->isEnabledBuiltinTax(); 86 | }); 87 | 88 | // Validate and initialize properties 89 | $this->initAttributes($attributes); 90 | } 91 | 92 | /** 93 | * Update information of cart item. 94 | * 95 | * @param array $attributes The new attributes 96 | * @param bool $withEvent Enable firing the event 97 | * 98 | * @return $this 99 | */ 100 | public function update(array $attributes = [], $withEvent = true) 101 | { 102 | // Determines the caller that called this method 103 | $caller = getCaller(); 104 | $callerClass = Arr::get($caller, 'class'); 105 | $creator = $this->getCreator(); 106 | 107 | // If the caller is not the creator of this instance 108 | if ($callerClass !== get_class($creator)) { 109 | return $creator->updateItem($this->getHash(), $attributes, $withEvent); 110 | } 111 | 112 | // Filter the allowed attributes to be updated 113 | if ($this->inCommercialCart) { 114 | $attributes = Arr::only($attributes, ['title', 'quantity', 'price', 'taxable', 'options', 'extra_info']); 115 | } else { 116 | $attributes = Arr::only($attributes, ['title', 'extra_info']); 117 | } 118 | 119 | // Validate input 120 | $this->validate($attributes); 121 | 122 | // Stores input into attributes 123 | $this->setAttributes($attributes); 124 | 125 | return $this; 126 | } 127 | 128 | /** 129 | * Get details of the item as a collection. 130 | * 131 | * @param bool $withActions Include details of applied actions in the result 132 | * 133 | * @return \Jackiedo\Cart\Details 134 | */ 135 | public function getDetails($withActions = true) 136 | { 137 | $details = [ 138 | 'hash' => $this->getHash(), 139 | 'associated_class' => $this->getAssociatedClass(), 140 | 'id' => $this->getId(), 141 | 'title' => $this->getTitle(), 142 | ]; 143 | 144 | if ($this->inCommercialCart) { 145 | $details['quantity'] = $this->getQuantity(); 146 | $details['price'] = $this->getPrice(); 147 | $details['taxable'] = $this->isTaxable(); 148 | $details['total_price'] = $this->getTotalPrice(); 149 | $details['actions_count'] = $this->countActions(); 150 | $details['actions_amount'] = $this->sumActionsAmount(); 151 | $details['subtotal'] = $this->getSubtotal(); 152 | 153 | if ($this->enabledBuiltinTax) { 154 | $details['taxable_amount'] = $this->getTaxableAmount(); 155 | } 156 | 157 | $details['options'] = new Details($this->getOptions()); 158 | 159 | if ($withActions) { 160 | $details['applied_actions'] = $this->getActionsContainer()->getDetails(); 161 | } 162 | } 163 | 164 | $details['extra_info'] = new Details($this->getExtraInfo()); 165 | 166 | return new Details($details); 167 | } 168 | 169 | /** 170 | * Determines which values ​​to filter. 171 | * 172 | * @return array 173 | */ 174 | public function getFilterValues() 175 | { 176 | return array_merge([ 177 | 'hash' => $this->getHash(), 178 | ], $this->attributes); 179 | } 180 | 181 | /** 182 | * Get the unique identifier of item in the cart. 183 | * 184 | * @return string 185 | */ 186 | public function getHash() 187 | { 188 | $id = $this->attributes['id']; 189 | $price = $this->attributes['price']; 190 | $associatedClass = $this->attributes['associated_class']; 191 | $options = $this->attributes['options']; 192 | 193 | ksort_recursive($options); 194 | 195 | return 'item_' . md5($id . serialize($price) . serialize($associatedClass) . serialize($options)); 196 | } 197 | 198 | /** 199 | * Get the model instance to which this item is associated. 200 | * 201 | * @return null|\Illuminate\Database\Eloquent 202 | * 203 | * @throws \Jackiedo\Cart\Exceptions\InvalidAssociatedException 204 | */ 205 | public function getModel() 206 | { 207 | $id = $this->attributes['id']; 208 | $associatedClass = $this->attributes['associated_class']; 209 | 210 | if (!class_exists($associatedClass)) { 211 | throw new InvalidAssociatedException('The [' . $associatedClass . '] class does not exist.'); 212 | } 213 | 214 | $model = with(new $associatedClass)->findById($id); 215 | 216 | if (!$model) { 217 | throw new InvalidModelException('The supplied associated model from [' . $associatedClass . '] does not exist.'); 218 | } 219 | 220 | return $model; 221 | } 222 | 223 | /** 224 | * Calculate the total price of item based on the quantity and unit price of item. 225 | * 226 | * @return float 227 | */ 228 | public function getToTalPrice() 229 | { 230 | return $this->attributes['quantity'] * $this->attributes['price']; 231 | } 232 | 233 | /** 234 | * Get the subtotal information of item in the cart. 235 | * 236 | * @return float 237 | */ 238 | public function getSubtotal() 239 | { 240 | return $this->getTotalPrice() + $this->sumActionsAmount(); 241 | } 242 | 243 | /** 244 | * Calculate taxable amount based on total price and total taxable action amounts. 245 | * 246 | * @return float 247 | */ 248 | public function getTaxableAmount() 249 | { 250 | if (!$this->enabledBuiltinTax || !$this->isTaxable()) { 251 | return 0; 252 | } 253 | 254 | return $this->getTotalPrice() + $this->sumActionsAmount([ 255 | 'rules' => [ 256 | 'taxable' => true, 257 | ], 258 | ]); 259 | } 260 | 261 | /** 262 | * Get value of one or some options of the item 263 | * using "dot" notation. 264 | * 265 | * @param null|array|string $options The option want to get 266 | * @param mixed $default 267 | * 268 | * @return mixed 269 | */ 270 | public function getOptions($options = null, $default = null) 271 | { 272 | $optionsAttribute = $this->attributes['options']; 273 | 274 | if (is_null($options)) { 275 | return $optionsAttribute; 276 | } 277 | 278 | if (is_array($options)) { 279 | return Arr::only($optionsAttribute, $options); 280 | } 281 | 282 | return Arr::get($optionsAttribute, $options, $default); 283 | } 284 | 285 | /** 286 | * Determines whether this is a taxable item. 287 | * 288 | * @return bool 289 | */ 290 | public function isTaxable() 291 | { 292 | return $this->attributes['taxable']; 293 | } 294 | 295 | /** 296 | * Initialize attributes for cart item. 297 | * 298 | * @param array $attributes The cart item attributes 299 | * 300 | * @return $this; 301 | * 302 | * @throws \Jackiedo\Cart\Exceptions\InvalidArgumentException 303 | */ 304 | protected function initAttributes(array $attributes = []) 305 | { 306 | // Disallow the associated_class key in the input attributes 307 | unset($attributes['associated_class']); 308 | 309 | // If UseCartable exists in the input attributes 310 | if (($model = Arr::get($attributes, 'model')) instanceof UseCartable) { 311 | $exceptAttrs = array_values(array_intersect(['title', 'price'], array_keys($attributes))); 312 | $attributes = array_merge($attributes, $this->parseUseCartable($model, $exceptAttrs)); 313 | } 314 | 315 | // Filter the attributes that allow initialization 316 | if ($this->inCommercialCart) { 317 | $attributes = Arr::only($attributes, ['id', 'title', 'quantity', 'price', 'taxable', 'options', 'extra_info', 'associated_class']); 318 | } else { 319 | $attributes = Arr::only($attributes, ['id', 'title', 'extra_info', 'associated_class']); 320 | $attributes['taxable'] = false; 321 | } 322 | 323 | // Add the missing attributes with default attributes 324 | $attributes = array_merge($this->attributes, $attributes); 325 | 326 | // Validate the attributes 327 | $this->validate($attributes); 328 | 329 | // Stores the input into attributes 330 | $this->setAttributes($attributes); 331 | 332 | // Creates the actions container 333 | $this->appliedActions = new ActionsContainer; 334 | 335 | return $this; 336 | } 337 | 338 | /** 339 | * Parse data from UseCartable model to retrieve attributes. 340 | * 341 | * @param object $model The UseCartable model 342 | * @param array $except The attrobutes will be excepted 343 | * 344 | * @return array 345 | */ 346 | protected function parseUseCartable($model, array $except = []) 347 | { 348 | $attributes = [ 349 | 'id' => $model->getUseCartableId(), 350 | 'title' => $model->getUseCartableTitle(), 351 | 'price' => $model->getUseCartablePrice(), 352 | 'associated_class' => get_class($model), 353 | ]; 354 | 355 | foreach ($except as $attribute) { 356 | unset($attributes[$attribute]); 357 | } 358 | 359 | return $attributes; 360 | } 361 | 362 | /** 363 | * Set value for the attributes of this instance. 364 | * 365 | * @param array $attributes 366 | * 367 | * @return void 368 | */ 369 | protected function setAttributes(array $attributes = []) 370 | { 371 | foreach ($attributes as $attribute => $value) { 372 | switch ($attribute) { 373 | case 'quantity': 374 | $this->setQuantity($value); 375 | break; 376 | 377 | case 'price': 378 | $this->setPrice($value); 379 | break; 380 | 381 | case 'options': 382 | $this->setOptions($value); 383 | break; 384 | 385 | case 'extra_info': 386 | $this->setExtraInfo($value); 387 | break; 388 | 389 | default: 390 | $this->attributes[$attribute] = $value; 391 | break; 392 | } 393 | } 394 | } 395 | 396 | /** 397 | * Set value for the quantity attribute. 398 | * 399 | * @param mixed $value 400 | * 401 | * @return void 402 | */ 403 | protected function setQuantity($value) 404 | { 405 | $this->attributes['quantity'] = intval($value); 406 | } 407 | 408 | /** 409 | * Set value for the price attribute. 410 | * 411 | * @param mixed $value 412 | * 413 | * @return void 414 | */ 415 | protected function setPrice($value) 416 | { 417 | $this->attributes['price'] = floatval($value); 418 | } 419 | 420 | /** 421 | * Set value for the options attribute. 422 | * 423 | * @param array $options 424 | * 425 | * @return void 426 | */ 427 | protected function setOptions(array $options = []) 428 | { 429 | if (empty($options)) { 430 | $this->attributes['options'] = []; 431 | } 432 | 433 | foreach ($options as $key => $value) { 434 | $key = trim($key, '.'); 435 | 436 | if (!empty($key)) { 437 | Arr::set($this->attributes['options'], $key, $value); 438 | } 439 | } 440 | } 441 | 442 | /** 443 | * Return the actions container. 444 | * 445 | * @return \Jackiedo\Cart\ActionsContainer 446 | */ 447 | protected function getActionsContainer() 448 | { 449 | if ($this->inCommercialCart) { 450 | return $this->appliedActions; 451 | } 452 | 453 | return new ActionsContainer; 454 | } 455 | 456 | /** 457 | * Indicates whether this instance can apply cart. 458 | * 459 | * @return bool 460 | */ 461 | protected function canApplyAction() 462 | { 463 | if ($this->inCommercialCart) { 464 | return true; 465 | } 466 | 467 | return false; 468 | } 469 | 470 | /** 471 | * Validate item attributes. 472 | * 473 | * @param array $attributes The item attributes 474 | * 475 | * @return void 476 | * 477 | * @throws \Jackiedo\Cart\Exceptions\InvalidArgumentException 478 | */ 479 | protected function validate(array $attributes = []) 480 | { 481 | if (array_key_exists('id', $attributes) && empty($attributes['id'])) { 482 | throw new InvalidArgumentException('The id attribute of the item is required.'); 483 | } 484 | 485 | if (array_key_exists('title', $attributes) && (!is_string($attributes['title']) || empty($attributes['title']))) { 486 | throw new InvalidArgumentException('The title attribute of the item is required.'); 487 | } 488 | 489 | if (array_key_exists('quantity', $attributes) && (!is_numeric($attributes['quantity']) || intval($attributes['quantity']) < 1)) { 490 | throw new InvalidArgumentException('The quantity attribute of the item is required and must be an integer type greater than 1.'); 491 | } 492 | 493 | if (array_key_exists('price', $attributes) && (!is_numeric($attributes['price']) || floatval($attributes['price']) < 0)) { 494 | throw new InvalidArgumentException('The price attribute of the item must be an float type greater than or equal to 0.'); 495 | } 496 | 497 | if (array_key_exists('taxable', $attributes) && !is_bool($attributes['taxable'])) { 498 | throw new InvalidArgumentException('The taxable attribute of the item must be a boolean type.'); 499 | } 500 | 501 | if (array_key_exists('options', $attributes) && !is_array($attributes['options'])) { 502 | throw new InvalidArgumentException('The options attribute of the item must be an array.'); 503 | } 504 | 505 | if (array_key_exists('extra_info', $attributes) && !is_array($attributes['extra_info'])) { 506 | throw new InvalidArgumentException('The extra_info attribute of the item must be an array.'); 507 | } 508 | } 509 | } 510 | -------------------------------------------------------------------------------- /src/ItemsContainer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class ItemsContainer extends Container 16 | { 17 | /** 18 | * The name of the accepted class is the creator. 19 | * 20 | * @var array 21 | */ 22 | protected $acceptedCreators = [ 23 | Cart::class, 24 | ]; 25 | 26 | /** 27 | * Get details information of this container as a collection. 28 | * 29 | * @param bool $withActions Include details of applied actions in the result 30 | * 31 | * @return \Jackiedo\Cart\Details 32 | */ 33 | public function getDetails($withActions = true) 34 | { 35 | $details = new Details; 36 | $allItems = $this->all(); 37 | 38 | foreach ($allItems as $hash => $item) { 39 | $details->put($hash, $item->getDetails($withActions)); 40 | } 41 | 42 | return $details; 43 | } 44 | 45 | /** 46 | * Add an item into this container. 47 | * 48 | * @param array $attributes The item attributes 49 | * @param bool $withEvent Enable firing the event 50 | * 51 | * @return null|\Jackiedo\Cart\Item 52 | */ 53 | public function addItem(array $attributes = [], $withEvent = true) 54 | { 55 | $item = new Item($attributes); 56 | 57 | if ($withEvent) { 58 | $event = $this->fireEvent('cart.item.adding', [$item]); 59 | 60 | if (false === $event) { 61 | return null; 62 | } 63 | } 64 | 65 | $itemHash = $item->getHash(); 66 | 67 | if ($this->has($itemHash)) { 68 | // If item is already exists in this container, we will increase quantity of item 69 | $newQuantity = $this->get($itemHash)->getQuantity() + $item->getQuantity(); 70 | 71 | return $this->updateItem($itemHash, ['quantity' => $newQuantity], $withEvent); 72 | } 73 | 74 | // If item is not exists in this container, we will put it to container 75 | $this->put($itemHash, $item); 76 | 77 | if ($withEvent) { 78 | $this->fireEvent('cart.item.added', [$item]); 79 | } 80 | 81 | return $item; 82 | } 83 | 84 | /** 85 | * Update item attributes of an item in this container. 86 | * 87 | * @param string $itemHash The unique identifier of item 88 | * @param array $attributes The new item attributes 89 | * @param bool $withEvent Enable firing the event 90 | * 91 | * @return null|\Jackiedo\Cart\Item 92 | */ 93 | public function updateItem($itemHash, $attributes = [], $withEvent = true) 94 | { 95 | if (!is_array($attributes)) { 96 | $attributes = ['quantity' => $attributes]; 97 | } 98 | 99 | if (array_key_exists('quantity', $attributes) && intval($attributes['quantity']) <= 0) { 100 | $this->removeItem($itemHash, $withEvent); 101 | 102 | return null; 103 | } 104 | 105 | $item = $this->getItem($itemHash); 106 | 107 | if ($withEvent) { 108 | $event = $this->fireEvent('cart.item.updating', [&$attributes, $item]); 109 | 110 | if (false === $event) { 111 | return null; 112 | } 113 | } 114 | 115 | $item->update($attributes); 116 | 117 | $newHash = $item->getHash(); 118 | 119 | if ($newHash != $itemHash) { 120 | $this->forget($itemHash); 121 | 122 | if ($this->has($newHash)) { 123 | $existingQty = $this->get($newHash)->getQuantity(); 124 | $attributes = array_merge($attributes, ['quantity' => $item->getQuantity() + $existingQty]); 125 | $item = $this->updateItem($newHash, $attributes, $withEvent); 126 | } else { 127 | $this->put($newHash, $item); 128 | } 129 | } 130 | 131 | if ($withEvent) { 132 | $this->fireEvent('cart.item.updated', [$item]); 133 | } 134 | 135 | return $item; 136 | } 137 | 138 | /** 139 | * Remove an item from this container. 140 | * 141 | * @param string $itemHash The unique identifier of item 142 | * @param bool $withEvent Enable firing the event 143 | * 144 | * @return $this 145 | */ 146 | public function removeItem($itemHash, $withEvent = true) 147 | { 148 | $item = $this->getItem($itemHash); 149 | 150 | if ($withEvent) { 151 | $event = $this->fireEvent('cart.item.removing', [$item]); 152 | 153 | if (false === $event) { 154 | return $this; 155 | } 156 | } 157 | 158 | $cart = $item->getCart(); 159 | $this->forget($itemHash); 160 | 161 | if ($withEvent) { 162 | $this->fireEvent('cart.item.removed', [$itemHash, clone $cart]); 163 | } 164 | 165 | return $this; 166 | } 167 | 168 | /** 169 | * Clear all item in this container. 170 | * 171 | * @param bool $withEvent Enable firing the event 172 | * 173 | * @return $this 174 | */ 175 | public function clearItems($withEvent = true) 176 | { 177 | $cart = $this->getCreator(); 178 | 179 | if ($withEvent) { 180 | $event = $this->fireEvent('cart.item.clearing', [$cart]); 181 | 182 | if (false === $event) { 183 | return $this; 184 | } 185 | } 186 | 187 | $this->forgetAll(); 188 | 189 | if ($withEvent) { 190 | $this->fireEvent('cart.item.cleared', [$cart]); 191 | } 192 | 193 | return $this; 194 | } 195 | 196 | /** 197 | * Get an item in this container by given hash. 198 | * 199 | * @param string $itemHash The unique identifier of item 200 | * 201 | * @return \Jackiedo\Cart\Item 202 | */ 203 | public function getItem($itemHash) 204 | { 205 | if (!$this->has($itemHash)) { 206 | $this->throwInvalidHashException($itemHash); 207 | } 208 | 209 | return $this->get($itemHash); 210 | } 211 | 212 | /** 213 | * Get all items in this container that match the given filter. 214 | * 215 | * @param mixed $filter The search filter 216 | * @param bool $complyAll indicates that the results returned must satisfy 217 | * all the conditions of the filter at the same time 218 | * or that only parts of the filter 219 | * 220 | * @return array 221 | */ 222 | public function getItems($filter = null, $complyAll = true) 223 | { 224 | // If there is no filter, return all items 225 | if (is_null($filter)) { 226 | return $this->all(); 227 | } 228 | 229 | // If filter is a closure 230 | if ($filter instanceof \Closure) { 231 | return $this->filter($filter)->all(); 232 | } 233 | 234 | // If filter is an array 235 | if (is_array($filter)) { 236 | // If filter is not an associative array 237 | if (!isAssocArray($filter)) { 238 | $filtered = $this->filter(function ($item) use ($filter) { 239 | return in_array($item->getHash(), $filter); 240 | }); 241 | 242 | return $filtered->all(); 243 | } 244 | 245 | // If filter is an associative 246 | if (!$complyAll) { 247 | $filtered = $this->filter(function ($item) use ($filter) { 248 | $intersects = array_intersect_assoc_recursive($item->getFilterValues(), $filter); 249 | 250 | return !empty($intersects); 251 | }); 252 | } else { 253 | $filtered = $this->filter(function ($item) use ($filter) { 254 | $diffs = array_diff_assoc_recursive($item->getFilterValues(), $filter); 255 | 256 | return empty($diffs); 257 | }); 258 | } 259 | 260 | return $filtered->all(); 261 | } 262 | 263 | return []; 264 | } 265 | 266 | /** 267 | * Count the number of items in this container that match the given filter. 268 | * 269 | * @param mixed $filter Search filter 270 | * @param bool $complyAll indicates that the results returned must satisfy 271 | * all the conditions of the filter at the same time 272 | * or that only parts of the filter 273 | * 274 | * @return int 275 | */ 276 | public function countItems($filter = null, $complyAll = true) 277 | { 278 | if ($this->isEmpty()) { 279 | return 0; 280 | } 281 | 282 | return count($this->getItems($filter, $complyAll)); 283 | } 284 | 285 | /** 286 | * Count the quantities of all items in this container that match the given filter. 287 | * 288 | * @param mixed $filter Search filter 289 | * @param bool $complyAll indicates that the results returned must satisfy 290 | * all the conditions of the filter at the same time 291 | * or that only parts of the filter 292 | * 293 | * @return int 294 | */ 295 | public function sumQuantity($filter = null, $complyAll = true) 296 | { 297 | if ($this->isEmpty()) { 298 | return 0; 299 | } 300 | 301 | $allItems = $this->getItems($filter, $complyAll); 302 | 303 | return array_reduce($allItems, function ($carry, $item) { 304 | return $carry + $item->getQuantity(); 305 | }, 0); 306 | } 307 | 308 | /** 309 | * Sum the subtotal of all items in this container that match the given filter. 310 | * 311 | * @param mixed $filter Search filter 312 | * @param bool $complyAll indicates that the results returned must satisfy 313 | * all the conditions of the filter at the same time 314 | * or that only parts of the filter 315 | * 316 | * @return float 317 | */ 318 | public function sumSubtotal($filter = null, $complyAll = true) 319 | { 320 | if ($this->isEmpty()) { 321 | return 0; 322 | } 323 | 324 | $allItems = $this->getItems($filter, $complyAll); 325 | 326 | return array_reduce($allItems, function ($carry, $item) { 327 | return $carry + $item->getSubtotal(); 328 | }, 0); 329 | } 330 | 331 | /** 332 | * Sum the taxable amount of all items in this container that match the given filter. 333 | * 334 | * @param mixed $filter Search filter 335 | * @param bool $complyAll indicates that the results returned must satisfy 336 | * all the conditions of the filter at the same time 337 | * or that only parts of the filter 338 | * 339 | * @return float 340 | */ 341 | public function sumTaxableAmount($filter = null, $complyAll = true) 342 | { 343 | if ($this->isEmpty()) { 344 | return 0; 345 | } 346 | 347 | $allItems = $this->getItems($filter, $complyAll); 348 | 349 | return array_reduce($allItems, function ($carry, $item) { 350 | return $carry + $item->getTaxableAmount(); 351 | }, 0); 352 | } 353 | } 354 | -------------------------------------------------------------------------------- /src/Tax.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class Tax implements CartNode 18 | { 19 | use CanBeCartNode; 20 | 21 | /** 22 | * The attributes of tax. 23 | * 24 | * @var array 25 | */ 26 | protected $attributes = [ 27 | 'id' => null, 28 | 'title' => null, 29 | 'rate' => null, 30 | 'extra_info' => [], 31 | ]; 32 | 33 | /** 34 | * The name of the accepted class is the creator. 35 | * 36 | * @var array 37 | */ 38 | protected $acceptedCreators = [ 39 | TaxesContainer::class, 40 | ]; 41 | 42 | /** 43 | * Create a new tax. 44 | * 45 | * @param array $attributes The tax attributes 46 | * 47 | * @return void 48 | */ 49 | public function __construct(array $attributes = []) 50 | { 51 | // Stores the creator 52 | $this->storeCreator(); 53 | 54 | // Initialize attributes 55 | $this->initAttributes($attributes); 56 | } 57 | 58 | /** 59 | * Update attributes of this tax instance. 60 | * 61 | * @param array $attributes The new attributes 62 | * @param bool $withEvent Enable firing the event 63 | * 64 | * @return $this 65 | */ 66 | public function update(array $attributes = [], $withEvent = true) 67 | { 68 | // Determines the caller that called this method 69 | $caller = getCaller(); 70 | $callerClass = Arr::get($caller, 'class'); 71 | $creator = $this->getCreator(); 72 | 73 | // If the caller is not the creator of this instance 74 | if ($callerClass !== get_class($creator)) { 75 | return $creator->updateTax($this->getHash(), $attributes, $withEvent); 76 | } 77 | 78 | // Filter the allowed attributes to be updated 79 | $attributes = Arr::only($attributes, ['title', 'rate', 'extra_info']); 80 | 81 | // Validate the input 82 | $this->validate($attributes); 83 | 84 | // Stores the input into attributes 85 | $this->setAttributes($attributes); 86 | 87 | return $this; 88 | } 89 | 90 | /** 91 | * Get details of tha tax as a collection. 92 | * 93 | * @return \Jackiedo\Cart\Details 94 | */ 95 | public function getDetails() 96 | { 97 | $details = [ 98 | 'hash' => $this->getHash(), 99 | 'id' => $this->getId(), 100 | 'title' => $this->getTitle(), 101 | 'rate' => $this->getRate(), 102 | 'amount' => $this->getAmount(), 103 | 'extra_info' => new Details($this->getExtraInfo()), 104 | ]; 105 | 106 | return new Details($details); 107 | } 108 | 109 | /** 110 | * Determines which values ​​to filter. 111 | * 112 | * @return array 113 | */ 114 | public function getFilterValues() 115 | { 116 | return array_merge([ 117 | 'hash' => $this->getHash(), 118 | ], $this->attributes); 119 | } 120 | 121 | /** 122 | * Get the unique identifier of the tax. 123 | * 124 | * @return string 125 | */ 126 | public function getHash() 127 | { 128 | return 'tax_' . md5($this->attributes['id']); 129 | } 130 | 131 | /** 132 | * Get the calculated amount of this tax. 133 | * 134 | * @return float 135 | */ 136 | public function getAmount() 137 | { 138 | try { 139 | $cartInstance = $this->getCart(); 140 | $taxableAmount = $cartInstance->getTaxableAmount(); 141 | 142 | return $taxableAmount * ($this->getRate() / 100); 143 | } catch (\Exception $e) { 144 | return 0; 145 | } 146 | } 147 | 148 | /** 149 | * Initialize the attributes. 150 | * 151 | * @param array $attributes The tax attributes 152 | * 153 | * @return $this 154 | */ 155 | protected function initAttributes(array $attributes = []) 156 | { 157 | // Define default value for attributes 158 | $this->attributes['rate'] = $this->getConfig('default_tax_rate', 0); 159 | 160 | // Add the missing attributes with default attributes 161 | $attributes = array_merge($this->attributes, Arr::only($attributes, array_keys($this->attributes))); 162 | 163 | // Validate the input 164 | $this->validate($attributes); 165 | 166 | // Stores the input into attributes 167 | $this->setAttributes($attributes); 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * Set value for the attributes of this instance. 174 | * 175 | * @param array $attributes 176 | * 177 | * @return void 178 | */ 179 | protected function setAttributes(array $attributes = []) 180 | { 181 | foreach ($attributes as $attribute => $value) { 182 | switch ($attribute) { 183 | case 'rate': 184 | $this->setRate($value); 185 | break; 186 | 187 | case 'extra_info': 188 | $this->setExtraInfo($value); 189 | break; 190 | 191 | default: 192 | $this->attributes[$attribute] = $value; 193 | break; 194 | } 195 | } 196 | } 197 | 198 | /** 199 | * Set value for the rate attribute. 200 | * 201 | * @param mixed $value 202 | * 203 | * @return void 204 | */ 205 | protected function setRate($value) 206 | { 207 | $this->attributes['rate'] = floatval($value); 208 | } 209 | 210 | /** 211 | * Validate the tax attributes. 212 | * 213 | * @param array $attributes The tax attributes 214 | * 215 | * @return void 216 | * 217 | * @throws \Jackiedo\Cart\Exceptions\InvalidArgumentException 218 | */ 219 | protected function validate($attributes) 220 | { 221 | if (array_key_exists('id', $attributes) && empty($attributes['id'])) { 222 | throw new InvalidArgumentException('The id attribute of the tax is required.'); 223 | } 224 | 225 | if (array_key_exists('title', $attributes) && (!is_string($attributes['title']) || empty($attributes['title']))) { 226 | throw new InvalidArgumentException('The title attribute of the tax is required and must be a non-empty string.'); 227 | } 228 | 229 | if (array_key_exists('rate', $attributes) && !preg_match('/^\d+(\.{0,1}\d+)?$/', $attributes['rate'])) { 230 | throw new InvalidArgumentException('The rate attribute of the tax is required and must be a float number greater than or equal to 0.'); 231 | } 232 | 233 | if (array_key_exists('extra_info', $attributes) && !is_array($attributes['extra_info'])) { 234 | throw new InvalidArgumentException('The extra_info attribute of the tax must be an array.'); 235 | } 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/TaxesContainer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class TaxesContainer extends Container 16 | { 17 | /** 18 | * The name of the accepted class is the creator. 19 | * 20 | * @var array 21 | */ 22 | protected $acceptedCreators = [ 23 | Cart::class, 24 | ]; 25 | 26 | /** 27 | * Add a tax instance into this container. 28 | * 29 | * @param array $attributes The tax attributes 30 | * @param bool $withEvent Enable firing the event 31 | * 32 | * @return null|\Jackiedo\Cart\Tax 33 | */ 34 | public function addTax(array $attributes = [], $withEvent = true) 35 | { 36 | $tax = new Tax($attributes); 37 | 38 | if ($withEvent) { 39 | $event = $this->fireEvent('cart.tax.applying', [$tax]); 40 | 41 | if (false === $event) { 42 | return null; 43 | } 44 | } 45 | 46 | $taxHash = $tax->getHash(); 47 | 48 | if ($this->has($taxHash)) { 49 | // If the tax is already exists in this container, we will update that tax 50 | return $this->updateTax($taxHash, $attributes, $withEvent); 51 | } 52 | 53 | // If the tax doesn't exist yet, put it to container 54 | $this->put($taxHash, $tax); 55 | 56 | if ($withEvent) { 57 | $this->fireEvent('cart.tax.applied', [$tax]); 58 | } 59 | 60 | return $tax; 61 | } 62 | 63 | /** 64 | * Update a tax in taxes container. 65 | * 66 | * @param string $taxHash The unique identifier of tax 67 | * @param array $attributes The new attributes 68 | * @param bool $withEvent Enable firing the event 69 | * 70 | * @return null|\Jackiedo\Cart\Tax 71 | */ 72 | public function updateTax($taxHash, array $attributes = [], $withEvent = true) 73 | { 74 | $tax = $this->getTax($taxHash); 75 | 76 | if ($withEvent) { 77 | $event = $this->fireEvent('cart.tax.updating', [&$attributes, $tax]); 78 | 79 | if (false === $event) { 80 | return null; 81 | } 82 | } 83 | 84 | $tax->update($attributes); 85 | 86 | $newHash = $tax->getHash(); 87 | 88 | if ($newHash != $taxHash) { 89 | $this->forget($taxHash); 90 | $this->put($newHash, $tax); 91 | } 92 | 93 | if ($withEvent) { 94 | $this->fireEvent('cart.tax.updated', [$tax]); 95 | } 96 | 97 | return $tax; 98 | } 99 | 100 | /** 101 | * Get a tax instance in this container by given hash. 102 | * 103 | * @param string $taxHash The unique identifier of tax instance 104 | * 105 | * @return \Jackiedo\Cart\Tax 106 | */ 107 | public function getTax($taxHash) 108 | { 109 | if (!$this->has($taxHash)) { 110 | $this->throwInvalidHashException($taxHash); 111 | } 112 | 113 | return $this->get($taxHash); 114 | } 115 | 116 | /** 117 | * Get all tax instance in this container that match the given filter. 118 | * 119 | * @param mixed $filter Search filter 120 | * @param bool $complyAll indicates that the results returned must satisfy 121 | * all the conditions of the filter at the same time 122 | * or that only parts of the filter 123 | * 124 | * @return array 125 | */ 126 | public function getTaxes($filter = null, $complyAll = true) 127 | { 128 | // If there is no filter, return all taxes 129 | if (is_null($filter)) { 130 | return $this->all(); 131 | } 132 | 133 | // If filter is a closure 134 | if ($filter instanceof \Closure) { 135 | return $this->filter($filter)->all(); 136 | } 137 | 138 | // If filter is an array 139 | if (is_array($filter)) { 140 | // If filter is not an associative array 141 | if (!isAssocArray($filter)) { 142 | $filtered = $this->filter(function ($tax) use ($filter) { 143 | return in_array($tax->getHash(), $filter); 144 | }); 145 | 146 | return $filtered->all(); 147 | } 148 | 149 | // If filter is an associative 150 | if (!$complyAll) { 151 | $filtered = $this->filter(function ($tax) use ($filter) { 152 | $intersects = array_intersect_assoc_recursive($tax->getFilterValues(), $filter); 153 | 154 | return !empty($intersects); 155 | }); 156 | } else { 157 | $filtered = $this->filter(function ($tax) use ($filter) { 158 | $diffs = array_diff_assoc_recursive($tax->getFilterValues(), $filter); 159 | 160 | return empty($diffs); 161 | }); 162 | } 163 | 164 | return $filtered->all(); 165 | } 166 | 167 | return []; 168 | } 169 | 170 | /** 171 | * Remove a tax instance from this container. 172 | * 173 | * @param string $taxHash The unique identifier of the tax instance 174 | * @param bool $withEvent Enable firing the event 175 | * 176 | * @return $this 177 | */ 178 | public function removeTax($taxHash, $withEvent = true) 179 | { 180 | $tax = $this->getTax($taxHash); 181 | 182 | if ($withEvent) { 183 | $event = $this->fireEvent('cart.tax.removing', [$tax]); 184 | 185 | if (false === $event) { 186 | return $this; 187 | } 188 | } 189 | 190 | $cart = $tax->getCart(); 191 | $this->forget($taxHash); 192 | 193 | if ($withEvent) { 194 | $this->fireEvent('cart.tax.removed', [$taxHash, clone $cart]); 195 | } 196 | 197 | return $this; 198 | } 199 | 200 | /** 201 | * Remove all tax instances from this container. 202 | * 203 | * @param bool $withEvent Enable firing the event 204 | * 205 | * @return $this 206 | */ 207 | public function clearTaxes($withEvent = true) 208 | { 209 | $cart = $this->getCreator(); 210 | 211 | if ($withEvent) { 212 | $event = $this->fireEvent('cart.tax.clearing', [$cart]); 213 | 214 | if (false === $event) { 215 | return $this; 216 | } 217 | } 218 | 219 | $this->forgetAll(); 220 | 221 | if ($withEvent) { 222 | $this->fireEvent('cart.tax.cleared', [$cart]); 223 | } 224 | 225 | return $this; 226 | } 227 | 228 | /** 229 | * Count all tax instances in this container that match the given filter. 230 | * 231 | * @param mixed $filter Search filter 232 | * @param bool $complyAll indicates that the results returned must satisfy 233 | * all the conditions of the filter at the same time 234 | * or that only parts of the filter 235 | * 236 | * @return int 237 | */ 238 | public function countTaxes($filter = null, $complyAll = true) 239 | { 240 | if ($this->isEmpty()) { 241 | return 0; 242 | } 243 | 244 | return count($this->getTaxes($filter, $complyAll)); 245 | } 246 | 247 | /** 248 | * Get the sum of tax rate for all tax instances in this container that match the given filter. 249 | * 250 | * @param mixed $filter Search filter 251 | * @param bool $complyAll indicates that the results returned must satisfy 252 | * all the conditions of the filter at the same time 253 | * or that only parts of the filter 254 | * 255 | * @return float 256 | */ 257 | public function sumRate($filter = null, $complyAll = true) 258 | { 259 | if ($this->isEmpty()) { 260 | return 0; 261 | } 262 | 263 | $allTaxes = $this->getTaxes($filter, $complyAll); 264 | 265 | return array_reduce($allTaxes, function ($carry, $tax) { 266 | return $carry + $tax->getRate(); 267 | }, 0); 268 | } 269 | 270 | /** 271 | * Get the sum of tax amount for all tax instances in this container that match the given filter. 272 | * 273 | * @param mixed $filter Search filter 274 | * @param bool $complyAll indicates that the results returned must satisfy 275 | * all the conditions of the filter at the same time 276 | * or that only parts of the filter 277 | * 278 | * @return float 279 | */ 280 | public function sumAmount($filter = null, $complyAll = true) 281 | { 282 | if ($this->isEmpty()) { 283 | return 0; 284 | } 285 | 286 | $allTaxes = $this->getTaxes($filter, $complyAll); 287 | 288 | return array_reduce($allTaxes, function ($carry, $tax) { 289 | return $carry + $tax->getAmount(); 290 | }, 0); 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /src/Traits/BackToCreator.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | trait BackToCreator 21 | { 22 | /** 23 | * Stores the creator instance. 24 | * 25 | * @var null|object 26 | */ 27 | protected $creator; 28 | 29 | /** 30 | * Get the creator of this instance. 31 | * 32 | * @return object 33 | * 34 | * @throws \Jackiedo\Cart\Exceptions\UnknownCreatorException 35 | */ 36 | public function getCreator() 37 | { 38 | if (!$this->hasKnownCreator()) { 39 | throw new UnknownCreatorException('The interacting instance does not belong to any cart tree.'); 40 | } 41 | 42 | return $this->creator; 43 | } 44 | 45 | /** 46 | * Determines weather this instance has stored the creator. 47 | * 48 | * @return bool 49 | */ 50 | public function hasKnownCreator() 51 | { 52 | return !is_null($this->creator); 53 | } 54 | 55 | /** 56 | * Stores the creator into instance. 57 | * 58 | * @param int $stepsBackward The steps backward from the method containing 59 | * this method to the constructor or the cloner. 60 | * It is 0 if this method is in the constructor 61 | * or the cloner. 62 | * @param callable $laterJob the action will be taken later if this instance 63 | * stored the creator 64 | * 65 | * @return $this 66 | */ 67 | protected function storeCreator($stepsBackward = 0, $laterJob = null) 68 | { 69 | $stepsBackward = max(0, $stepsBackward); 70 | $caller = getCaller(__CLASS__ . '::' . __FUNCTION__, 1 + $stepsBackward); 71 | $callerClass = Arr::get($caller, 'class'); 72 | $callerObject = Arr::get($caller, 'object'); 73 | $acceptedCreators = is_array($this->acceptedCreators) ? $this->acceptedCreators : []; 74 | 75 | if (in_array($callerClass, $acceptedCreators) && is_object($callerObject)) { 76 | $this->creator = (Cart::class == $callerClass) ? clone $callerObject : $callerObject; 77 | 78 | if ($laterJob instanceof \Closure) { 79 | call_user_func_array($laterJob, [$this->creator, $caller]); 80 | } 81 | } 82 | 83 | return $this; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Traits/CanApplyAction.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | trait CanApplyAction 13 | { 14 | /** 15 | * Add an action into the actions container. 16 | * 17 | * @param array $attributes The action attributes 18 | * @param bool $withEvent Enable firing the event 19 | * 20 | * @return null|\Jackiedo\Cart\Action 21 | */ 22 | public function applyAction(array $attributes = [], $withEvent = true) 23 | { 24 | if (!$this->canApplyAction()) { 25 | return null; 26 | } 27 | 28 | return $this->getActionsContainer()->addAction($attributes, $withEvent); 29 | } 30 | 31 | /** 32 | * Update an action in the actions container. 33 | * 34 | * @param string $actionHash The unique identifier of the action 35 | * @param array $attributes The new attributes 36 | * @param bool $withEvent Enable firing the event 37 | * 38 | * @return null|\Jackiedo\Cart\Action 39 | */ 40 | public function updateAction($actionHash, array $attributes = [], $withEvent = true) 41 | { 42 | return $this->getActionsContainer()->updateAction($actionHash, $attributes, $withEvent); 43 | } 44 | 45 | /** 46 | * Determines if the action exists in the actions container by given action hash. 47 | * 48 | * @param string $actionHash The unique identifier of the action 49 | * 50 | * @return bool 51 | */ 52 | public function hasAction($actionHash) 53 | { 54 | return $this->getActionsContainer()->has($actionHash); 55 | } 56 | 57 | /** 58 | * Get an action in the actions container. 59 | * 60 | * @param string $actionHash The unique identifier of the action 61 | * 62 | * @return \Jackiedo\Cart\Action 63 | */ 64 | public function getAction($actionHash) 65 | { 66 | return $this->getActionsContainer()->getAction($actionHash); 67 | } 68 | 69 | /** 70 | * Get all actions in the actions container that match the given filter. 71 | * 72 | * @param mixed $filter Search filter 73 | * @param bool $complyAll indicates that the results returned must satisfy 74 | * all the conditions of the filter at the same time 75 | * or that only parts of the filter 76 | * 77 | * @return array 78 | */ 79 | public function getActions($filter = null, $complyAll = true) 80 | { 81 | return $this->getActionsContainer()->getActions($filter, $complyAll); 82 | } 83 | 84 | /** 85 | * Remove an action from the actions container. 86 | * 87 | * @param string $actionHash The unique identifier of the action 88 | * @param bool $withEvent Enable firing the event 89 | * 90 | * @return $this 91 | */ 92 | public function removeAction($actionHash, $withEvent = true) 93 | { 94 | $this->getActionsContainer()->removeAction($actionHash, $withEvent); 95 | 96 | return $this; 97 | } 98 | 99 | /** 100 | * Remove all actions from the actions container. 101 | * 102 | * @param bool $withEvent Enable firing the event 103 | * 104 | * @return $this 105 | */ 106 | public function clearActions($withEvent = true) 107 | { 108 | $this->getActionsContainer()->clearActions($withEvent); 109 | 110 | return $this; 111 | } 112 | 113 | /** 114 | * Count all actions in the actions container that match the given filter. 115 | * 116 | * @param mixed $filter Search filter 117 | * @param bool $complyAll indicates that the results returned must satisfy 118 | * all the conditions of the filter at the same time 119 | * or that only parts of the filter 120 | * 121 | * @return int 122 | */ 123 | public function countActions($filter = null, $complyAll = true) 124 | { 125 | return $this->getActionsContainer()->countActions($filter, $complyAll); 126 | } 127 | 128 | /** 129 | * Calculate the sum of action amounts in the actions container that match the given filter. 130 | * 131 | * @param mixed $filter Search filter 132 | * @param bool $complyAll indicates that the results returned must satisfy 133 | * all the conditions of the filter at the same time 134 | * or that only parts of the filter 135 | * 136 | * @return float 137 | */ 138 | public function sumActionsAmount($filter = null, $complyAll = true) 139 | { 140 | return $this->getActionsContainer()->sumAmount($filter, $complyAll); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Traits/CanBeCartNode.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | trait CanBeCartNode 17 | { 18 | use BackToCreator { getCreator as protected; } 19 | 20 | /** 21 | * Dynamically handle calls to the class. 22 | * 23 | * @param string $method The method name 24 | * @param array $parameters The input parameters 25 | * 26 | * @return mixed 27 | */ 28 | public function __call($method, $parameters) 29 | { 30 | if (strlen($method) > 3 && 'get' == substr($method, 0, 3)) { 31 | $attribute = Str::snake(substr($method, 3)); 32 | 33 | if (array_key_exists($attribute, $this->attributes)) { 34 | return $this->attributes[$attribute]; 35 | } 36 | } 37 | 38 | throw new \BadMethodCallException(sprintf( 39 | 'Method %s::%s does not exist.', 40 | static::class, 41 | $method 42 | )); 43 | } 44 | 45 | /** 46 | * Check if the parent node can be found. 47 | * 48 | * @return bool 49 | */ 50 | public function hasKnownParentNode() 51 | { 52 | return $this->hasKnownCreator() && $this->getCreator()->hasKnownCreator(); 53 | } 54 | 55 | /** 56 | * Get parent node instance that this instance is belong to. 57 | * 58 | * @return object 59 | */ 60 | public function getParentNode() 61 | { 62 | return $this->getCreator()->getCreator(); 63 | } 64 | 65 | /** 66 | * Get the cart instance that this node belong to. 67 | * 68 | * @return Jackiedo\Cart\Cart 69 | */ 70 | public function getCart() 71 | { 72 | $parentNode = $this->getParentNode(); 73 | 74 | if ($parentNode instanceof Cart) { 75 | return $parentNode; 76 | } 77 | 78 | return $parentNode->getCart(); 79 | } 80 | 81 | /** 82 | * Get config of the cart instance thet this node belong to. 83 | * 84 | * @param null|string $name The config name 85 | * @param mixed $default The return value if the config does not exist 86 | * 87 | * @return mixed 88 | */ 89 | public function getConfig($name = null, $default = null) 90 | { 91 | try { 92 | return $this->getCart()->getConfig($name, $default); 93 | } catch (\Exception $e) { 94 | return $default; 95 | } 96 | } 97 | 98 | /** 99 | * Get the cart node's original attribute values. 100 | * 101 | * @param null|string $attribute The attribute 102 | * @param mixed $default The return value if attribute does not exist 103 | * 104 | * @return mixed 105 | */ 106 | public function getOriginal($attribute = null, $default = null) 107 | { 108 | if ($attribute) { 109 | return Arr::get($this->attributes, $attribute, $default); 110 | } 111 | 112 | return $this->attributes; 113 | } 114 | 115 | /** 116 | * Dynamic attribute getter. 117 | * 118 | * @param null|string $attribute The attribute 119 | * @param mixed $default The return value if attribute does not exist 120 | * 121 | * @return mixed 122 | */ 123 | public function get($attribute, $default = null) 124 | { 125 | if (!empty($attribute)) { 126 | $getMethod = Str::camel('get_' . $attribute); 127 | 128 | if (method_exists($this, $getMethod)) { 129 | $methodReflection = new \ReflectionMethod($this, $getMethod); 130 | $isMethodPublic = $methodReflection->isPublic(); 131 | $numberOfRequiredParams = $methodReflection->getNumberOfRequiredParameters(); 132 | 133 | if ($isMethodPublic && 0 == $numberOfRequiredParams) { 134 | return $this->{$getMethod}(); 135 | } 136 | } 137 | 138 | return $this->getOriginal($attribute, $default); 139 | } 140 | 141 | return $default; 142 | } 143 | 144 | /** 145 | * Get value of one or some extended informations of the current node 146 | * using "dot" notation. 147 | * 148 | * @param null|array|string $information The information want to get 149 | * @param mixed $default 150 | * 151 | * @return mixed 152 | */ 153 | public function getExtraInfo($information = null, $default = null) 154 | { 155 | $extraInfo = $this->attributes['extra_info']; 156 | 157 | if (is_null($information)) { 158 | return $extraInfo; 159 | } 160 | 161 | if (is_array($information)) { 162 | return Arr::only($extraInfo, $information); 163 | } 164 | 165 | return Arr::get($extraInfo, $information, $default); 166 | } 167 | 168 | /** 169 | * Set value for an attribute of this node. 170 | * 171 | * @param string $attribute The attribute want to set 172 | * @param mixed $value The value of attribute 173 | * 174 | * @return void 175 | */ 176 | protected function setAttribute($attribute, $value) 177 | { 178 | if (!empty($attribute)) { 179 | $setter = Str::camel('set_' . $attribute); 180 | 181 | if (method_exists($this, $setter)) { 182 | $this->{$setter}($value); 183 | } else { 184 | $this->attributes[$attribute] = $value; 185 | } 186 | } 187 | } 188 | 189 | /** 190 | * Set value for the attributes of this node. 191 | * 192 | * @param array $attributes 193 | * 194 | * @return void 195 | */ 196 | protected function setAttributes(array $attributes = []) 197 | { 198 | foreach ($attributes as $attribute => $value) { 199 | $this->setAttribute($attribute, $value); 200 | } 201 | } 202 | 203 | /** 204 | * Set value for extended informations of the current node. 205 | * Can use "dot" notation with each information. 206 | * 207 | * @param array $informations 208 | * 209 | * @return void 210 | */ 211 | protected function setExtraInfo(array $informations = []) 212 | { 213 | if (empty($informations)) { 214 | $this->attributes['extra_info'] = []; 215 | } 216 | 217 | foreach ($informations as $key => $value) { 218 | $key = trim($key, '.'); 219 | 220 | if (!empty($key)) { 221 | Arr::set($this->attributes['extra_info'], $key, $value); 222 | } 223 | } 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/Traits/CanUseCart.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | trait CanUseCart 16 | { 17 | /** 18 | * Add the UseCartable item to the cart. 19 | * 20 | * @param \Jackiedo\Cart\Cart|string $cartOrName The cart instance or the name of the cart 21 | * @param array $attributes The additional attributes 22 | * @param bool $withEvent Enable firing the event 23 | * 24 | * @return null|\Jackiedo\Cart\Item 25 | */ 26 | public function addToCart($cartOrName, array $attributes = [], $withEvent = true) 27 | { 28 | $cart = ($cartOrName instanceof Cart) ? $cartOrName : CartFacade::newInstance($cartOrName); 29 | 30 | return $cart->addItem(array_merge($attributes, ['model' => $this]), $withEvent); 31 | } 32 | 33 | /** 34 | * Determines the UseCartable item has in the cart. 35 | * 36 | * @param \Jackiedo\Cart\Cart|string $cartOrName The cart instance or the name of the cart 37 | * @param array $filter Array of additional filter 38 | * 39 | * @return bool 40 | */ 41 | public function hasInCart($cartOrName, array $filter = []) 42 | { 43 | $foundInCart = $this->searchInCart($cartOrName, $filter); 44 | 45 | return !empty($foundInCart); 46 | } 47 | 48 | /** 49 | * Get all the UseCartable item in the cart. 50 | * 51 | * @param \Jackiedo\Cart\Cart|string $cartOrName The cart instance or the name of the cart 52 | * 53 | * @return array 54 | */ 55 | public function allFromCart($cartOrName) 56 | { 57 | return $this->searchInCart($cartOrName); 58 | } 59 | 60 | /** 61 | * Get the UseCartable items in the cart with given additional options. 62 | * 63 | * @param \Jackiedo\Cart\Cart|string $cartOrName The cart instance or the name of the cart 64 | * @param array $filter Array of additional filter 65 | * 66 | * @return array 67 | */ 68 | public function searchInCart($cartOrName, array $filter = []) 69 | { 70 | $cart = ($cartOrName instanceof Cart) ? $cartOrName : CartFacade::newInstance($cartOrName); 71 | $filter = array_merge($filter, [ 72 | 'id' => $this->getUseCartableId(), 73 | 'associated_class' => __CLASS__, 74 | ]); 75 | 76 | return $cart->getItems($filter, true); 77 | } 78 | 79 | /** 80 | * Get the identifier of the UseCartable item. 81 | * 82 | * @return int|string 83 | */ 84 | public function getUseCartableId() 85 | { 86 | return method_exists($this, 'getKey') ? $this->getKey() : $this->id; 87 | } 88 | 89 | /** 90 | * Get the title of the UseCartable item. 91 | * 92 | * @return string 93 | */ 94 | public function getUseCartableTitle() 95 | { 96 | if (property_exists($this, 'title')) { 97 | return $this->title; 98 | } 99 | 100 | if (property_exists($this, 'cartTitleField')) { 101 | return $this->getAttribute($this->cartTitleField); 102 | } 103 | 104 | return 'Unknown'; 105 | } 106 | 107 | /** 108 | * Get the price of the UseCartable item. 109 | * 110 | * @return float 111 | */ 112 | public function getUseCartablePrice() 113 | { 114 | if (property_exists($this, 'price')) { 115 | return $this->price; 116 | } 117 | 118 | if (property_exists($this, 'cartPriceField')) { 119 | return $this->getAttribute($this->cartPriceField); 120 | } 121 | 122 | return 0; 123 | } 124 | 125 | /** 126 | * Find a model by its identifier. 127 | * 128 | * @param int $id The identifier of model 129 | * 130 | * @return null|\Illuminate\Support\Collection|static 131 | */ 132 | public function findById($id) 133 | { 134 | return $this->find($id); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Traits/CollectionForgetAll.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | trait CollectionForgetAll 13 | { 14 | /** 15 | * Remove all items out of the collection. 16 | * 17 | * @return $this 18 | */ 19 | public function forgetAll() 20 | { 21 | parent::__construct(); 22 | 23 | return $this; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Traits/FireEvent.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | trait FireEvent 13 | { 14 | /** 15 | * Fire an event and call the listeners. 16 | * 17 | * @param object|string $event 18 | * @param mixed $payload 19 | * @param bool $halt 20 | * 21 | * @return null|array 22 | */ 23 | protected function fireEvent($event, $payload = [], $halt = true) 24 | { 25 | return event($event, $payload, $halt); 26 | } 27 | } 28 | --------------------------------------------------------------------------------