├── .gitignore ├── CanBeBought.php ├── Cart.php ├── CartItem.php ├── CartItemOptions.php ├── CartServiceProvider.php ├── Plugin.php ├── Vagrantfile ├── assets └── css │ └── product-form.css ├── components ├── Basket.php ├── ComponentBase.php ├── Product.php ├── Products.php ├── basket │ ├── _item.htm │ └── default.htm ├── product │ └── default.htm └── products │ ├── default.htm │ └── product.htm ├── contracts └── Buyable.php ├── controllers ├── Products.php └── products │ ├── _list_toolbar.htm │ ├── _product_toolbar.htm │ ├── config_filter.yaml │ ├── config_form.yaml │ ├── config_list.yaml │ ├── create.htm │ ├── index.htm │ └── update.htm ├── exceptions ├── CartAlreadyStoredException.php ├── InvalidRowIDException.php └── UnknownModelException.php ├── facades └── Cart.php ├── lang ├── de │ └── lang.php └── en │ └── lang.php ├── models ├── CurrencySettings.php ├── Product.php ├── ShopSetting.php ├── currencysettings │ └── fields.yaml ├── product │ ├── columns.yaml │ └── fields.yaml └── shopsetting │ └── fields.yaml ├── traits └── Uuidable.php ├── updates ├── Migration.php ├── create_products_table.php └── version.yaml ├── util ├── Currency.php ├── Image.php ├── UrlMaker.php └── Uuid.php └── vagrant └── bootstrap.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | .vagrant/ 4 | composer.lock 5 | *-cloudimg-console.log 6 | -------------------------------------------------------------------------------- /CanBeBought.php: -------------------------------------------------------------------------------- 1 | getKey() : $this->id; 13 | } 14 | 15 | /** 16 | * Get the description or title of the Buyable item. 17 | * 18 | * @return string 19 | */ 20 | public function getBuyableDescription() 21 | { 22 | foreach (['name', 'title', 'dsecription'] as $key) { 23 | if (property_exists($this, $prop)) { 24 | return $this->$prop; 25 | } 26 | } 27 | 28 | return null; 29 | } 30 | 31 | /** 32 | * Get the price of the Buyable item. 33 | * 34 | * @return float 35 | */ 36 | public function getBuyablePrice() 37 | { 38 | return property_exists($this, 'price') ? $this->price : null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Cart.php: -------------------------------------------------------------------------------- 1 | session = $session; 58 | $this->events = $events; 59 | 60 | $this->instance(self::DEFAULT_INSTANCE); 61 | } 62 | 63 | /** 64 | * Set the current cart instance. 65 | * 66 | * @param string|null $instance 67 | * @return \Octoshop\Core\Cart 68 | */ 69 | public function instance($instance = null) 70 | { 71 | $instance = $instance ?: self::DEFAULT_INSTANCE; 72 | 73 | $this->instance = sprintf('%s.%s', 'cart', $instance); 74 | 75 | return $this; 76 | } 77 | 78 | /** 79 | * Get the current cart instance. 80 | * 81 | * @return string 82 | */ 83 | public function currentInstance() 84 | { 85 | return str_replace('cart.', '', $this->instance); 86 | } 87 | 88 | /** 89 | * Add an item to the cart. 90 | * 91 | * @param mixed $id 92 | * @param mixed $name 93 | * @param int|float $qty 94 | * @param float $price 95 | * @param array $options 96 | * @return \Octoshop\Core\CartItem 97 | */ 98 | public function add($id, $name = null, $qty = null, $price = null, array $options = []) 99 | { 100 | if ($this->isMulti($id)) { 101 | return array_map(function ($item) { 102 | return $this->add($item); 103 | }, $id); 104 | } 105 | 106 | $cartItem = $this->createCartItem($id, $name, $qty, $price, $options); 107 | 108 | $content = $this->getContent(); 109 | 110 | if ($content->has($cartItem->rowId)) { 111 | $cartItem->qty += $content->get($cartItem->rowId)->qty; 112 | } 113 | 114 | $content->put($cartItem->rowId, $cartItem); 115 | 116 | $this->events->fire('cart.added', $cartItem); 117 | 118 | $this->session->put($this->instance, $content); 119 | 120 | return $cartItem; 121 | } 122 | 123 | /** 124 | * Update the cart item with the given rowId. 125 | * 126 | * @param string $rowId 127 | * @param mixed $qty 128 | * @return void 129 | */ 130 | public function update($rowId, $qty) 131 | { 132 | $cartItem = $this->get($rowId); 133 | 134 | if ($qty instanceof Buyable) { 135 | $cartItem->updateFromBuyable($qty); 136 | } elseif (is_array($qty)) { 137 | $cartItem->updateFromArray($qty); 138 | } else { 139 | $cartItem->qty = $qty; 140 | } 141 | 142 | $content = $this->getContent(); 143 | 144 | if ($rowId !== $cartItem->rowId) { 145 | $content->pull($rowId); 146 | 147 | if ($content->has($cartItem->rowId)) { 148 | $existingCartItem = $this->get($cartItem->rowId); 149 | $cartItem->setQuantity($existingCartItem->qty + $cartItem->qty); 150 | } 151 | } 152 | 153 | if ($cartItem->qty <= 0) { 154 | $this->remove($cartItem->rowId); 155 | return; 156 | } else { 157 | $content->put($cartItem->rowId, $cartItem); 158 | } 159 | 160 | $this->events->fire('cart.updated', $cartItem); 161 | 162 | $this->session->put($this->instance, $content); 163 | } 164 | 165 | /** 166 | * Remove the cart item with the given rowId from the cart. 167 | * 168 | * @param string $rowId 169 | * @return void 170 | */ 171 | public function remove($rowId) 172 | { 173 | $cartItem = $this->get($rowId); 174 | 175 | $content = $this->getContent(); 176 | 177 | $content->pull($cartItem->rowId); 178 | 179 | $this->events->fire('cart.removed', $cartItem); 180 | 181 | $this->session->put($this->instance, $content); 182 | } 183 | 184 | /** 185 | * Get a cart item from the cart by its rowId. 186 | * 187 | * @param string $rowId 188 | * @return \Octoshop\Core\CartItem 189 | */ 190 | public function get($rowId) 191 | { 192 | $content = $this->getContent(); 193 | 194 | if (!$content->has($rowId)) 195 | throw new InvalidRowIDException( 196 | sprintf(Lang::get('octoshop.core::lang.cart.invalid_row'), $rowId) 197 | ); 198 | 199 | return $content->get($rowId); 200 | } 201 | 202 | /** 203 | * Destroy the current cart instance. 204 | * 205 | * @return void 206 | */ 207 | public function destroy() 208 | { 209 | $this->session->remove($this->instance); 210 | } 211 | 212 | /** 213 | * Get the content of the cart. 214 | * 215 | * @return \Illuminate\Support\Collection 216 | */ 217 | public function content() 218 | { 219 | if (is_null($this->session->get($this->instance))) { 220 | return new Collection([]); 221 | } 222 | 223 | return $this->session->get($this->instance); 224 | } 225 | 226 | /** 227 | * Get the number of items in the cart. 228 | * 229 | * @return int|float 230 | */ 231 | public function count() 232 | { 233 | $content = $this->getContent(); 234 | 235 | return $content->sum('qty'); 236 | } 237 | 238 | /** 239 | * Get the total price of the items in the cart. 240 | * 241 | * @return string 242 | */ 243 | public function total() 244 | { 245 | $content = $this->getContent(); 246 | 247 | $total = $content->reduce(function ($total, CartItem $cartItem) { 248 | return $total + ($cartItem->qty * $cartItem->priceTax); 249 | }, 0); 250 | 251 | return $total; 252 | } 253 | 254 | /** 255 | * Get the total tax of the items in the cart. 256 | * 257 | * @return float 258 | */ 259 | public function tax() 260 | { 261 | $content = $this->getContent(); 262 | 263 | $tax = $content->reduce(function ($tax, CartItem $cartItem) { 264 | return $tax + ($cartItem->qty * $cartItem->tax); 265 | }, 0); 266 | 267 | return $tax; 268 | } 269 | 270 | /** 271 | * Get the subtotal (total - tax) of the items in the cart. 272 | * 273 | * @return float 274 | */ 275 | public function subtotal() 276 | { 277 | $content = $this->getContent(); 278 | 279 | $subTotal = $content->reduce(function ($subTotal, CartItem $cartItem) { 280 | return $subTotal + ($cartItem->qty * $cartItem->price); 281 | }, 0); 282 | 283 | return $subTotal; 284 | } 285 | 286 | /** 287 | * Search the cart content for a cart item matching the given search closure. 288 | * 289 | * @param \Closure $search 290 | * @return \Illuminate\Support\Collection 291 | */ 292 | public function search(Closure $search) 293 | { 294 | $content = $this->getContent(); 295 | 296 | return $content->filter($search); 297 | } 298 | 299 | /** 300 | * Associate the cart item with the given rowId with the given model. 301 | * 302 | * @param string $rowId 303 | * @param mixed $model 304 | * @return void 305 | */ 306 | public function associate($rowId, $model) 307 | { 308 | if(is_string($model) && !class_exists($model)) { 309 | throw new UnknownModelException( 310 | sprintf(Lang::get('octoshop.core::cart.invalid_model'), $model) 311 | ); 312 | } 313 | 314 | $cartItem = $this->get($rowId); 315 | 316 | $cartItem->associate($model); 317 | 318 | $content = $this->getContent(); 319 | 320 | $content->put($cartItem->rowId, $cartItem); 321 | 322 | $this->session->put($this->instance, $content); 323 | } 324 | 325 | /** 326 | * Check the cart to ensure its items are able to be purchased. 327 | * 328 | * @return array 329 | */ 330 | public function validate() 331 | { 332 | // Ensure a clean slate 333 | $this->callbacks = []; 334 | $errors = []; 335 | 336 | // Register availability check internally 337 | // to make sure that it runs before others. 338 | $this->registerItemValidator(function(CartItem $item) { 339 | $error = null; 340 | $product = $item->product(); 341 | 342 | if (!$product->is_enabled) { 343 | $error = sprintf(Lang::get('octoshop.core::lang.cart.product_disabled'), $item->name); 344 | } elseif ($product->cannotBePurchased()) { 345 | $error = sprintf(Lang::get('octoshop.core::lang.cart.product_unavailable'), $item->name); 346 | } elseif ($product->minimum_qty > $item->qty) { 347 | $error = sprintf(Lang::get('octoshop.core::lang.cart.product_quota'), $item->name, $product->minimum_qty); 348 | } 349 | 350 | return $error ?: true; 351 | }); 352 | 353 | Event::fire('cart.validate_items', [$this]); 354 | 355 | foreach ($this->getContent() as $item) { 356 | $errors = array_merge($errors, $this->validateItem($item)); 357 | } 358 | 359 | return $errors; 360 | } 361 | 362 | protected function validateItem($item) 363 | { 364 | $errors = []; 365 | 366 | foreach ($this->callbacks as $callback) { 367 | $result = call_user_func_array($callback, [$item]); 368 | 369 | if ($result === true) { 370 | continue; 371 | } 372 | 373 | $errors[] = $result; 374 | } 375 | 376 | return $errors; 377 | } 378 | 379 | public function registerItemValidator($callback) 380 | { 381 | $this->callbacks[] = $callback; 382 | } 383 | 384 | /** 385 | * Set the tax rate for the cart item with the given rowId. 386 | * 387 | * @param string $rowId 388 | * @param int|float $taxRate 389 | * @return void 390 | */ 391 | public function setTax($rowId, $taxRate) 392 | { 393 | $cartItem = $this->get($rowId); 394 | 395 | $cartItem->setTaxRate($taxRate); 396 | 397 | $content = $this->getContent(); 398 | 399 | $content->put($cartItem->rowId, $cartItem); 400 | 401 | $this->session->put($this->instance, $content); 402 | } 403 | 404 | /** 405 | * Magic method to make accessing the total, tax and subtotal properties possible. 406 | * 407 | * @param string $attribute 408 | * @return float|null 409 | */ 410 | public function __get($attribute) 411 | { 412 | if (in_array(['tax', 'total', 'subtotal'], $attribute)) { 413 | return $this->$attribute(2, '.', ''); 414 | } 415 | 416 | return null; 417 | } 418 | 419 | /** 420 | * Get the carts content, if there is no cart content set yet, return a new empty Collection 421 | * 422 | * @return \Illuminate\Support\Collection 423 | */ 424 | protected function getContent() 425 | { 426 | return $this->session->get($this->instance, new Collection); 427 | } 428 | 429 | /** 430 | * Create a new CartItem from the supplied attributes. 431 | * 432 | * @param mixed $id 433 | * @param mixed $name 434 | * @param int|float $qty 435 | * @param float $price 436 | * @param array $options 437 | * @return \Octoshop\Core\CartItem 438 | */ 439 | private function createCartItem($id, $name, $qty, $price, array $options) 440 | { 441 | if ($id instanceof Buyable) { 442 | $cartItem = CartItem::fromBuyable($id, $qty ?: []); 443 | $cartItem->setQuantity($name ?: 1); 444 | $cartItem->associate($id); 445 | } elseif (is_array($id)) { 446 | $cartItem = CartItem::fromArray($id); 447 | $cartItem->setQuantity($id['qty']); 448 | } else { 449 | $cartItem = CartItem::fromAttributes($id, $name, $price, $options); 450 | $cartItem->setQuantity($qty); 451 | } 452 | 453 | $cartItem->setTaxRate(config('cart.tax')); 454 | 455 | return $cartItem; 456 | } 457 | 458 | /** 459 | * Check if the item is a multidimensional array or an array of Buyables. 460 | * 461 | * @param mixed $item 462 | * @return bool 463 | */ 464 | private function isMulti($item) 465 | { 466 | if (!is_array($item)) { 467 | return false; 468 | } 469 | 470 | return is_array(head($item)) || head($item) instanceof Buyable; 471 | } 472 | } 473 | -------------------------------------------------------------------------------- /CartItem.php: -------------------------------------------------------------------------------- 1 | id = $id; 86 | $this->name = $name; 87 | $this->price = floatval($price); 88 | $this->options = new CartItemOptions($options); 89 | $this->rowId = $this->generateRowId($id, $options); 90 | } 91 | 92 | /** 93 | * Returns the formatted price without TAX. 94 | * 95 | * @return string 96 | */ 97 | public function price() 98 | { 99 | return $this->price; 100 | } 101 | 102 | /** 103 | * Returns the formatted price with TAX. 104 | * 105 | * @return string 106 | */ 107 | public function priceTax() 108 | { 109 | return $this->priceTax; 110 | } 111 | 112 | /** 113 | * Returns the formatted subtotal. 114 | * Subtotal is price for whole CartItem without TAX 115 | * 116 | * @return string 117 | */ 118 | public function subtotal() 119 | { 120 | return $this->subtotal; 121 | } 122 | 123 | /** 124 | * Returns the formatted total. 125 | * Total is price for whole CartItem with TAX 126 | * 127 | * @return string 128 | */ 129 | public function total() 130 | { 131 | return $this->total; 132 | } 133 | 134 | /** 135 | * Returns the formatted tax. 136 | * 137 | * @return string 138 | */ 139 | public function tax() 140 | { 141 | return $this->tax; 142 | } 143 | 144 | /** 145 | * Returns the formatted tax. 146 | * 147 | * @return string 148 | */ 149 | public function taxTotal() 150 | { 151 | return $this->taxTotal; 152 | } 153 | 154 | /** 155 | * Set the quantity for this cart item. 156 | * 157 | * @param int|float $qty 158 | */ 159 | public function setQuantity($qty) 160 | { 161 | if(empty($qty) || ! is_numeric($qty)) 162 | throw new \InvalidArgumentException(Lang::get('octoshop.core::lang.cart.invalid_quantity')); 163 | 164 | $this->qty = $qty; 165 | } 166 | 167 | /** 168 | * Update the cart item from a Buyable. 169 | * 170 | * @param \Octoshop\Core\Contracts\Buyable $item 171 | * @return void 172 | */ 173 | public function updateFromBuyable(Buyable $item) 174 | { 175 | $this->id = $item->getBuyableIdentifier(); 176 | $this->name = $item->getBuyableDescription(); 177 | $this->price = $item->getBuyablePrice(); 178 | $this->priceTax = $this->price + $this->tax; 179 | } 180 | 181 | /** 182 | * Update the cart item from an array. 183 | * 184 | * @param array $attributes 185 | * @return void 186 | */ 187 | public function updateFromArray(array $attributes) 188 | { 189 | $this->id = array_get($attributes, 'id', $this->id); 190 | $this->qty = array_get($attributes, 'qty', $this->qty); 191 | $this->name = array_get($attributes, 'name', $this->name); 192 | $this->price = array_get($attributes, 'price', $this->price); 193 | $this->priceTax = $this->price + $this->tax; 194 | $this->options = new CartItemOptions(array_get($attributes, 'options', [])); 195 | 196 | $this->rowId = $this->generateRowId($this->id, $this->options->all()); 197 | } 198 | 199 | /** 200 | * Associate the cart item with the given model. 201 | * 202 | * @param mixed $model 203 | * @return void 204 | */ 205 | public function associate($model) 206 | { 207 | $this->associatedModel = is_string($model) ? $model : get_class($model); 208 | } 209 | 210 | /** 211 | * Set the tax rate. 212 | * 213 | * @param int|float $taxRate 214 | * @return void 215 | */ 216 | public function setTaxRate($taxRate) 217 | { 218 | $this->taxRate = $taxRate; 219 | } 220 | 221 | /** 222 | * Get an attribute from the cart item or get the associated model. 223 | * 224 | * @param string $attribute 225 | * @return mixed 226 | */ 227 | public function __get($attribute) 228 | { 229 | if(property_exists($this, $attribute)) { 230 | return $this->{$attribute}; 231 | } 232 | 233 | switch ($attribute) { 234 | case 'priceTax': return $this->price + $this->tax; 235 | case 'subtotal': return $this->qty * $this->price; 236 | case 'total': return $this->qty * $this->priceTax; 237 | case 'tax': return $this->price * ($this->taxRate / 100); 238 | case 'taxTotal': return $this->tax * $this->qty; 239 | } 240 | 241 | return null; 242 | } 243 | 244 | public function product() 245 | { 246 | return with(new $this->associatedModel)->find($this->id); 247 | } 248 | 249 | /** 250 | * Create a new instance from a Buyable. 251 | * 252 | * @param \Octoshop\Core\Contracts\Buyable $item 253 | * @param array $options 254 | * @return \Octoshop\Core\CartItem 255 | */ 256 | public static function fromBuyable(Buyable $item, array $options = []) 257 | { 258 | return new self($item->getBuyableIdentifier(), $item->getBuyableDescription(), $item->getBuyablePrice(), $options); 259 | } 260 | 261 | /** 262 | * Create a new instance from the given array. 263 | * 264 | * @param array $attributes 265 | * @return \Octoshop\Core\CartItem 266 | */ 267 | public static function fromArray(array $attributes) 268 | { 269 | $options = array_get($attributes, 'options', []); 270 | 271 | return new self($attributes['id'], $attributes['name'], $attributes['price'], $options); 272 | } 273 | 274 | /** 275 | * Create a new instance from the given attributes. 276 | * 277 | * @param int|string $id 278 | * @param string $name 279 | * @param float $price 280 | * @param array $options 281 | * @return \Octoshop\Core\CartItem 282 | */ 283 | public static function fromAttributes($id, $name, $price, array $options = []) 284 | { 285 | return new self($id, $name, $price, $options); 286 | } 287 | 288 | /** 289 | * Generate a unique id for the cart item. 290 | * 291 | * @param string $id 292 | * @param array $options 293 | * @return string 294 | */ 295 | protected function generateRowId($id, array $options) 296 | { 297 | ksort($options); 298 | 299 | return md5($id . serialize($options)); 300 | } 301 | 302 | /** 303 | * Get the instance as an array. 304 | * 305 | * @return array 306 | */ 307 | public function toArray() 308 | { 309 | return [ 310 | 'rowId' => $this->rowId, 311 | 'id' => $this->id, 312 | 'name' => $this->name, 313 | 'qty' => $this->qty, 314 | 'price' => $this->price, 315 | 'options' => $this->options, 316 | 'tax' => $this->tax, 317 | 'subtotal' => $this->subtotal 318 | ]; 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /CartItemOptions.php: -------------------------------------------------------------------------------- 1 | get($key); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CartServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->bind('cart', 'Octoshop\Core\Cart'); 17 | 18 | $this->app['events']->listen(Logout::class, function () { 19 | if (false) { 20 | $this->app->make(SessionManager::class)->forget('cart'); 21 | } 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Plugin.php: -------------------------------------------------------------------------------- 1 | alias('Cart', '\Octoshop\Core\Facades\Cart'); 17 | } 18 | 19 | public function pluginDetails() 20 | { 21 | return [ 22 | 'name' => 'octoshop.core::lang.plugin.name', 23 | 'icon' => 'icon-shopping-cart', 24 | 'author' => 'Dave Shoreman', 25 | 'homepage' => 'http://octoshop.co/', 26 | 'description' => 'octoshop.core::lang.plugin.description', 27 | ]; 28 | } 29 | 30 | public function registerComponents() 31 | { 32 | $this->components = [ 33 | 'Octoshop\Core\Components\Basket' => 'shopBasket', 34 | 'Octoshop\Core\Components\Products' => 'shopProducts', 35 | 'Octoshop\Core\Components\Product' => 'shopProduct', 36 | ]; 37 | 38 | Event::fire('octoshop.core.extendComponents', [$this]); 39 | 40 | return $this->components; 41 | } 42 | 43 | public function addComponents($components) 44 | { 45 | return $this->components = array_replace($this->components, $components); 46 | } 47 | 48 | public function registerMarkupTags() 49 | { 50 | return [ 51 | 'filters' => [ 52 | 'currency' => ['Octoshop\Core\Util\Currency', 'format'], 53 | 'thumbnail' => ['Octoshop\Core\Util\Image', 'thumbnail'], 54 | ], 55 | ]; 56 | } 57 | 58 | public function registerNavigation() 59 | { 60 | return [ 61 | 'octoshop' => [ 62 | 'label' => 'octoshop.core::lang.plugin.menu_label', 63 | 'url' => Backend::url('octoshop/core/products'), 64 | 'icon' => 'icon-shopping-cart', 65 | 'order' => 300, 66 | 'permissions' => [ 67 | 'octoshop.core.*', 68 | ], 69 | 'sideMenu' => [ 70 | 'products' => [ 71 | 'label' => 'octoshop.core::lang.menu.products', 72 | 'url' => Backend::url('octoshop/core/products'), 73 | 'icon' => 'icon-cubes', 74 | 'order' => 200, 75 | 'permissions' => ['octoshop.core.access_products'] 76 | ], 77 | ], 78 | ], 79 | ]; 80 | } 81 | 82 | public function registerPermissions() 83 | { 84 | return [ 85 | 'octoshop.core.access_products' => [ 86 | 'tab' => 'octoshop.core::lang.plugin.name', 87 | 'label' => 'octoshop.core::lang.permissions.products', 88 | ], 89 | 'octoshop.core.access_settings' => [ 90 | 'tab' => 'octoshop.core::lang.plugin.name', 91 | 'label' => 'octoshop.core::lang.permissions.settings', 92 | ], 93 | ]; 94 | } 95 | 96 | public function registerSettings() 97 | { 98 | return [ 99 | 'currency' => [ 100 | 'label' => 'octoshop.core::lang.settings.currency.label', 101 | 'description' => 'octoshop.core::lang.settings.currency.description', 102 | 'category' => 'octoshop.core::lang.plugin.name', 103 | 'icon' => 'icon-gbp', 104 | 'class' => 'Octoshop\Core\Models\CurrencySettings', 105 | 'order' => 160, 106 | 'permissions' => ['octoshop.core.access_settings'], 107 | ], 108 | 'shop' => [ 109 | 'label' => 'octoshop.core::lang.settings.shop.label', 110 | 'description' => 'octoshop.core::lang.settings.shop.description', 111 | 'category' => 'octoshop.core::lang.plugin.name', 112 | 'icon' => 'icon-sliders', 113 | 'class' => 'Octoshop\Core\Models\ShopSetting', 114 | 'order' => 150, 115 | 'permissions' => ['octoshop.core.access_settings'], 116 | ], 117 | ]; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | Vagrant.configure(2) do |config| 5 | config.vm.box = "ubuntu/xenial64" 6 | 7 | config.vm.network "forwarded_port", guest: 80, host: 8100 8 | 9 | config.vm.provider "virtualbox" do |vb| 10 | vb.name = "Octoshop Core Devel" 11 | vb.linked_clone = true 12 | vb.memory = 1024 13 | end 14 | 15 | # Disable this line on first run of vagrant up. 16 | # It won't be able to provision otherwise because composer 17 | # needs to create the october project above the shared folder. 18 | config.vm.synced_folder "../addressbook", "/var/www/octoshop.dev/plugins/octoshop/addressbook", 19 | create: true, group: "www-data", owner: "www-data" 20 | #disabled: true 21 | config.vm.synced_folder "../base-theme", "/var/www/octoshop.dev/themes/base", 22 | create: true, group: "www-data", owner: "www-data" 23 | #disabled: true 24 | config.vm.synced_folder "../checkout", "/var/www/octoshop.dev/plugins/octoshop/checkout", 25 | create: true, group: "www-data", owner: "www-data" 26 | #disabled: true 27 | config.vm.synced_folder "./", "/var/www/octoshop.dev/plugins/octoshop/core", 28 | create: true, group: "www-data", owner: "www-data" 29 | #disabled: true 30 | config.vm.synced_folder "../geozones", "/var/www/octoshop.dev/plugins/octoshop/geozones", 31 | create: true, group: "www-data", owner: "www-data" 32 | #disabled: true 33 | config.vm.synced_folder "../payment", "/var/www/octoshop.dev/plugins/octoshop/payment", 34 | create: true, group: "www-data", owner: "www-data" 35 | #disabled: true 36 | config.vm.synced_folder "../seo", "/var/www/octoshop.dev/plugins/octoshop/seo", 37 | create: true, group: "www-data", owner: "www-data" 38 | #disabled: true 39 | config.vm.synced_folder "../shipping", "/var/www/octoshop.dev/plugins/octoshop/shipping", 40 | create: true, group: "www-data", owner: "www-data" 41 | #disabled: true 42 | config.vm.synced_folder "../stock", "/var/www/octoshop.dev/plugins/octoshop/stock", 43 | create: true, group: "www-data", owner: "www-data" 44 | #disabled: true 45 | config.vm.synced_folder "../treecat", "/var/www/octoshop.dev/plugins/octoshop/treecat", 46 | create: true, group: "www-data", owner: "www-data" 47 | #disabled: true 48 | config.vm.synced_folder "../units", "/var/www/octoshop.dev/plugins/octoshop/units", 49 | create: true, group: "www-data", owner: "www-data" 50 | #disabled: true 51 | 52 | config.vm.provision :shell, path: "vagrant/bootstrap.sh" 53 | end 54 | -------------------------------------------------------------------------------- /assets/css/product-form.css: -------------------------------------------------------------------------------- 1 | #Form-secondaryTabs .tab-pane.layout-cell { 2 | padding: 20px 20px 0 20px; 3 | } 4 | -------------------------------------------------------------------------------- /components/Basket.php: -------------------------------------------------------------------------------- 1 | 'octoshop.core::lang.components.basket.name', 26 | 'description' => 'octoshop.core::lang.components.basket.description' 27 | ]; 28 | } 29 | 30 | public function defineProperties() 31 | { 32 | return [ 33 | 'basketContainer' => [ 34 | 'title' => 'octoshop.core::lang.components.basket.basketContainer', 35 | 'description' => 'octoshop.core::lang.components.basket.basketContainer_description', 36 | 'default' => '#basket', 37 | ], 38 | 'basketPartial' => [ 39 | 'title' => 'octoshop.core::lang.components.basket.basketPartial', 40 | 'description' => 'octoshop.core::lang.components.basket.basketPartial_description', 41 | 'default' => 'basket/default', 42 | ], 43 | 'condense' => [ 44 | 'title' => 'octoshop.core::lang.components.basket.condense', 45 | 'description' => 'octoshop.core::lang.components.basket.condense_description', 46 | 'type' => 'string', 47 | ], 48 | ]; 49 | } 50 | 51 | public function onRun() 52 | { 53 | $this->prepareVars(); 54 | } 55 | 56 | public function prepareVars() 57 | { 58 | $this->setPageProp('condense'); 59 | 60 | $this->refresh(); 61 | } 62 | 63 | public function refresh() 64 | { 65 | $this->setPageProp('basketContainer'); 66 | $this->setPageProp('basketPartial'); 67 | $this->setPageProp('basketItems', Cart::content()); 68 | $this->setPageProp('basketCount', $count = Cart::count() ?: 0); 69 | $this->setPageProp('basketTotal', $total = Cart::total() ?: 0); 70 | 71 | return [ 72 | 'count' => $count, 73 | 'total' => $total, 74 | ]; 75 | } 76 | 77 | public function onAddProduct() 78 | { 79 | $product = ShopProduct::find($id = post('id')); 80 | 81 | Cart::add( 82 | $product->id, 83 | $product->title, 84 | post('quantity', 1), 85 | $product->price 86 | )->associate($product); 87 | 88 | return $this->refresh(); 89 | } 90 | 91 | public function onSetProductQty() 92 | { 93 | Cart::update(post('row_id'), post('quantity')); 94 | 95 | return $this->refresh(); 96 | } 97 | 98 | public function onRemoveProduct() 99 | { 100 | Cart::remove(post('row_id')); 101 | 102 | return $this->refresh(); 103 | } 104 | 105 | public function onEmptyBasket() 106 | { 107 | Cart::destroy(); 108 | 109 | return $this->refresh(); 110 | } 111 | 112 | public function onGoToCheckout() 113 | { 114 | $this->prepareVars(); 115 | 116 | $basketErrors = Cart::validate(); 117 | 118 | if (count($basketErrors) > 0) { 119 | throw new ValidationException($basketErrors); 120 | } 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /components/ComponentBase.php: -------------------------------------------------------------------------------- 1 | property($property); 15 | 16 | $this->page[$property] = $value; 17 | $this->{$property} = $value; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /components/Product.php: -------------------------------------------------------------------------------- 1 | 'octoshop.core::lang.components.product.name', 19 | 'description' => 'octoshop.core::lang.components.product.description', 20 | ]; 21 | } 22 | 23 | public function defineProperties() 24 | { 25 | return [ 26 | 'slug' => [ 27 | 'title' => 'octoshop.core::lang.components.product.slug', 28 | 'default' => '{{ :slug }}', 29 | 'type' => 'string', 30 | ], 31 | 'basket' => [ 32 | 'title' => 'octoshop.core::lang.components.product.basket', 33 | 'description' => 'octoshop.core::lang.components.product.basket_description', 34 | ], 35 | 'isPrimary' => [ 36 | 'title' => 'octoshop.core::lang.components.product.isPrimary', 37 | 'type' => 'checkbox', 38 | ], 39 | ]; 40 | } 41 | 42 | public function onRun() 43 | { 44 | $this->prepareVars(); 45 | 46 | try { 47 | $product = $this->loadProduct(); 48 | 49 | $this->setPageProp('product', $product); 50 | } catch (ModelNotFoundException $e) { 51 | $this->setStatusCode(404); 52 | 53 | return $this->controller->run(404); 54 | } 55 | } 56 | 57 | public function prepareVars() 58 | { 59 | $this->setPageProp('slug'); 60 | $this->setPageProp('basket'); 61 | } 62 | 63 | public function loadProduct() 64 | { 65 | return ShopProduct::findBySlug($this->slug)->firstEnabledWithImages(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /components/Products.php: -------------------------------------------------------------------------------- 1 | [ 10 | 'title' => 'octoshop.core::lang.components.products.productPage', 11 | 'description' => 'octoshop.core::lang.components.products.productPage_description', 12 | 'type' => 'dropdown', 13 | 'default' => 'product', 14 | ], 15 | ]; 16 | 17 | protected $preparedVars = []; 18 | 19 | protected $productFilters; 20 | 21 | public $products; 22 | 23 | public function componentDetails() 24 | { 25 | return [ 26 | 'name' => 'octoshop.core::lang.components.products.name', 27 | 'description' => 'octoshop.core::lang.components.products.description', 28 | ]; 29 | } 30 | 31 | public function defineProperties() 32 | { 33 | return $this->componentProperties; 34 | } 35 | 36 | public function addProperties($properties) 37 | { 38 | $this->componentProperties = array_replace($this->componentProperties, $properties); 39 | 40 | return $this->properties = $this->validateProperties([]); 41 | } 42 | 43 | public function getProductPageOptions() 44 | { 45 | return Page::sortBy('baseFileName')->lists('baseFileName', 'baseFileName'); 46 | } 47 | 48 | public function onRun() 49 | { 50 | $this->prepareVars(); 51 | } 52 | 53 | public function prepareVars() 54 | { 55 | $this->registerVar('productPage', $this->property('productPage')); 56 | 57 | foreach ($this->preparedVars as $var) { 58 | $value = is_callable($var->value) ? call_user_func($var->value) : $var->value; 59 | 60 | $this->setPageProp($var->name, $value); 61 | } 62 | 63 | $this->setPageProp('products', $this->listProducts()); 64 | } 65 | 66 | public function registerVar($var, $value) 67 | { 68 | array_push($this->preparedVars, (object) [ 69 | 'name' => $var, 70 | 'value' => $value, 71 | ]); 72 | } 73 | 74 | public function listProducts() 75 | { 76 | $products = new Product; 77 | 78 | foreach ($this->productFilters as $property => $method) { 79 | $products = $products->$method($this->property($property)); 80 | } 81 | 82 | /** 83 | * @todo: Change is_visible to a dropdown similar to is_available 84 | * In other words, 0 => Hidden, 1 => Visible, 2 => Visible once available 85 | */ 86 | return $products->allVisibleWithImages(); 87 | } 88 | 89 | public function registerFilter($property, $scope) 90 | { 91 | $this->productFilters[$property] = $scope; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /components/basket/_item.htm: -------------------------------------------------------------------------------- 1 |
5 | | Product | 6 |Qty. | 7 |Price | 8 |Subtotal | 9 |
---|---|---|---|---|
Total: | 17 |{{ __SELF__.basketTotal|currency }} | 18 |
{{ product.tagline }}
5 | {% endif %} 6 | 7 | {% if product.model %} 8 |Model: {{ product.model }}
Coming soon! Please check back later.
36 | {% else %} 37 |Sorry, this product is currently unavailable for purchase.
38 | {% endif %} 39 | -------------------------------------------------------------------------------- /components/products/default.htm: -------------------------------------------------------------------------------- 1 | {% for product in __SELF__.products %} 2 | {% partial __SELF__~'::product' product=product %} 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /components/products/product.htm: -------------------------------------------------------------------------------- 1 |{{ product.tagline }}
5 | {% endif %} 6 | 7 |Coming soon! Please check back later.
23 | {% else %} 24 |This product is currently unavailable.
25 | {% endif %} 26 | -------------------------------------------------------------------------------- /contracts/Buyable.php: -------------------------------------------------------------------------------- 1 | addCss($this->assetsPath.'/css/product-form.css'); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /controllers/products/_list_toolbar.htm: -------------------------------------------------------------------------------- 1 |15 | 16 | = e(trans('octoshop.core::lang.product.return_to_products')) ?> 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /controllers/products/index.htm: -------------------------------------------------------------------------------- 1 | = $this->listRender() ?> 2 | -------------------------------------------------------------------------------- /controllers/products/update.htm: -------------------------------------------------------------------------------- 1 | fatalError): ?> 2 | 3 |15 | 16 | = e(trans('octoshop.core::lang.product.return_to_products')) ?> 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /exceptions/CartAlreadyStoredException.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'name' => 'Octoshop Core', 6 | 'menu_label' => 'Shop', 7 | 'description' => 'Eine maßgeschneiderte, modulare eCommerce Lösung für jeden Anwendungsfall.', 8 | ], 9 | 'menu' => [ 10 | 'products' => 'Produkte', 11 | ], 12 | 'cart' => [ 13 | 'product_disabled' => '"%s" konnte nicht gefunden werden.', 14 | 'product_unavailable' => '"%s" ist aktuell nicht verfügbar.', 15 | 'product_quota' => '"%s" muss mindestens %s mal bestellt werden.', 16 | 'invalid_row' => 'Im Einkaufswagen gibt es kein Produkt mit ID "%s".', 17 | 'invalid_model' => 'The supplied model "%s" does not exist.', 18 | 'invalid_id' => 'Bitte gib eine valide ID an.', 19 | 'invalid_name' => 'Bitte gib einen validen Namen an.', 20 | 'invalid_price' => 'Bitte gib eeinen validen Preis an.', 21 | 'invalid_quantity' => 'Bitte gib eine valide Anzahl an.', 22 | ], 23 | 'migration' => [ 24 | 'invalid_version' => 'Ungültige Version', 25 | ], 26 | 'permissions' => [ 27 | 'products' => 'Produkte verwalten', 28 | 'settings' => 'Shop-Einstellungen verwalten', 29 | ], 30 | 'urlmaker' => [ 31 | 'no_primary_component' => 'Keine Komponente "%s" gefunden um URL in %s zu generieren.', 32 | ], 33 | 'components' => [ 34 | 'basket' => [ 35 | 'name' => 'Einkaufswagen', 36 | 'description' => "Inhalt des Einkaufswagen anzeigen und verarbeiten.", 37 | 'basketContainer' => 'Einkaufwagen Container', 38 | 'basketContainer_description' => 'CSS Selektor zum Update des Einkaufwagen-Containers.', 39 | 'basketPartial' => 'Einkaufswagen Partial', 40 | 'basketPartial_description' => 'Partial, um dem Einkaufswagen Produkte hinzuzufügen.', 41 | 'condense' => 'Gekürzte Anzeige', 42 | 'condense_description' => 'Zeigt die Standard-Tabelle mit CSS Klasse "table-condensed" an.', 43 | ], 44 | 'product' => [ 45 | 'name' => 'Produkt', 46 | 'description' => 'Einzelnes Produkt anzeigen.', 47 | 'slug' => 'Slug', 48 | 'basket' => 'Einkaufwagen Container', 49 | 'basket_description' => 'CSS Selektor zum Update des Einkaufwagen-Containers.', 50 | 'isPrimary' => 'Hervorgehoben?', 51 | ], 52 | 'products' => [ 53 | 'name' => 'Alle Produkte', 54 | 'description' => 'Alle Produkte in einer Liste anzeigen.', 55 | 'productPage' => 'Produktseite', 56 | 'productPage_description' => 'Name der Produktseite um die URLs zu den Produkten zu generieren.', 57 | ], 58 | ], 59 | 'product' => [ 60 | 'label' => 'Produkt', 61 | 'tabs' => [ 62 | 'manage' => 'Informationen', 63 | 'edit' => 'Bearbeiten', 64 | 'images' => 'Bilder', 65 | ], 66 | 'id' => 'ID', 67 | 'title' => 'Titel', 68 | 'title_placeholder' => 'Neues Produkt', 69 | 'slug' => 'Slug', 70 | 'tagline' => 'Tagline', 71 | 'tagline_placeholder' => 'Gib eine kurzes Beschreibung des Produkts ein.', 72 | 'model' => 'Modell', 73 | 'description' => 'Beschreibung', 74 | 'isEnabled' => 'Dieses Produkt aktivieren', 75 | 'isEnabled_comment' => 'Stelle den Schalter of OFF um das Produkt zu deaktivieren.', 76 | 'isAvailable' => 'Verfügbarkeit', 77 | 'available' => 'Jetzt verfügbar', 78 | 'unavailable' => 'Nicht verfügbar', 79 | 'coming_soon' => 'Bald verfügbar', 80 | 'isVisible' => 'Sichtbarkeit', 81 | 'isVisible_comment' => 'Stelle den Schalter auf OFF, um das Produkt auf der Webseite zu verbergen.', 82 | 'availableAt' => 'Verfügbar ab', 83 | 'price' => 'Basis Preis', 84 | 'price_comment' => 'Gib den Preis ohne Steuern, Versandkosten und Ermäßigungen ein.', 85 | 'minimumQty' => 'Mindestbestellmenge', 86 | 'createdAt' => 'Erstellt', 87 | 'updatedAt' => 'Bearbeitet', 88 | 'saving' => 'Produkt speichern...', 89 | 'deleting' => 'Produkt löschen...', 90 | 'delete_confirm' => 'Möchtest du dieses Produkt wirklich löschen?', 91 | 'return_to_products' => 'Zurück zu den Produkten', 92 | ], 93 | 'products' => [ 94 | 'manage' => 'Produkte bearbeiten', 95 | 'new' => 'Neues Produkt', 96 | 'options' => [ 97 | 'label' => 'Weitere Optionen', 98 | 'forum' => 'Support-Forum (englisch)', 99 | ], 100 | 'enabled' => 'Aktiviert', 101 | 'visible' => 'Sichtbar', 102 | 'available' => 'Verfügbar', 103 | ], 104 | 'settings' => [ 105 | 'currency' => [ 106 | 'label' => 'Währung', 107 | 'description' => 'Wähle den Währungs Code und das Symbol.', 108 | 'name' => 'Name der Währung', 109 | 'code' => 'ISO Code', 110 | 'decimals' => 'Dezimalstellen', 111 | 'prefix' => 'Linkes Symbol', 112 | 'suffix' => 'Rechtes Symbol', 113 | 'thousand_separator' => 'Tausender Trennzeichen', 114 | 'decimal_separator' => 'Dezimal Trennzeichen', 115 | ], 116 | 'shop' => [ 117 | 'label' => 'Shop Einstellungen', 118 | 'description' => 'Verschiedene Einstellungen die das Front-End des Shops betreffen.', 119 | 'tab' => 'Produkte', 120 | 'defaultProductImage' => 'Standard Produktbild', 121 | ], 122 | ], 123 | ]; 124 | -------------------------------------------------------------------------------- /lang/en/lang.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'name' => 'Octoshop Core', 6 | 'menu_label' => 'Shop', 7 | 'description' => 'A bespoke, modular eCommerce solution fit for any purpose.', 8 | ], 9 | 'menu' => [ 10 | 'products' => 'Products', 11 | ], 12 | 'cart' => [ 13 | 'product_disabled' => '"%s" could not be found.', 14 | 'product_unavailable' => '"%s" is currently unavailable.', 15 | 'product_quota' => '"%s" requires a minimum of %s to order.', 16 | 'invalid_row' => 'The cart does not contain rowId "%s".', 17 | 'invalid_model' => 'The supplied model "%s" does not exist.', 18 | 'invalid_id' => 'Please supply a valid identifier.', 19 | 'invalid_name' => 'Please supply a valid name.', 20 | 'invalid_price' => 'Please supply a valid price.', 21 | 'invalid_quantity' => 'Please supply a valid quantity.', 22 | ], 23 | 'migration' => [ 24 | 'invalid_version' => 'Invalid version', 25 | ], 26 | 'permissions' => [ 27 | 'products' => 'Manage shop products', 28 | 'settings' => 'Manage shop settings', 29 | ], 30 | 'urlmaker' => [ 31 | 'no_primary_component' => 'Unable to a find a primary component "%s" for generating a URL in %s.', 32 | ], 33 | 'components' => [ 34 | 'basket' => [ 35 | 'name' => 'Basket', 36 | 'description' => "Show the contents of and process the user's basket.", 37 | 'basketContainer' => 'Basket container', 38 | 'basketContainer_description' => 'CSS selector of the basket container element to update.', 39 | 'basketPartial' => 'Basket partial', 40 | 'basketPartial_description' => 'Partial to use when adding products to basket.', 41 | 'condense' => 'Condensed view', 42 | 'condense_description' => 'Renders the default table with the table-condensed class.', 43 | ], 44 | 'product' => [ 45 | 'name' => 'Product', 46 | 'description' => 'Display a single product.', 47 | 'slug' => 'Slug', 48 | 'basket' => 'Basket container element', 49 | 'basket_description' => 'CSS identifier to update when adding products to cart.', 50 | 'isPrimary' => 'Is primary?', 51 | ], 52 | 'products' => [ 53 | 'name' => 'Product List', 54 | 'description' => 'Displays a list of products on the page.', 55 | 'productPage' => 'Product', 56 | 'productPage_description' => 'Name of the product page file for the product links.', 57 | ], 58 | ], 59 | 'product' => [ 60 | 'label' => 'Product', 61 | 'tabs' => [ 62 | 'manage' => 'Manage', 63 | 'edit' => 'Edit', 64 | 'images' => 'Images', 65 | ], 66 | 'id' => 'ID', 67 | 'title' => 'Title', 68 | 'title_placeholder' => 'New product name', 69 | 'slug' => 'Slug', 70 | 'tagline' => 'Tagline', 71 | 'tagline_placeholder' => 'Enter a short description to introduce people to your product', 72 | 'model' => 'Model', 73 | 'description' => 'Description', 74 | 'isEnabled' => 'Enable this product', 75 | 'isEnabled_comment' => 'Switch this off to completely disable the product.', 76 | 'isAvailable' => 'Availability', 77 | 'available' => 'Available now', 78 | 'unavailable' => 'Unavailable', 79 | 'coming_soon' => 'Available from set date', 80 | 'isVisible' => 'Show in listings', 81 | 'isVisible_comment' => 'Disable this option to prevent the product showing on category pages.', 82 | 'availableAt' => 'Available from', 83 | 'price' => 'Base Price', 84 | 'price_comment' => 'Enter the price before tax, shipping and discounts are applied.', 85 | 'minimumQty' => 'Minimum Order Quantity', 86 | 'createdAt' => 'Date Created', 87 | 'updatedAt' => 'Last Update', 88 | 'saving' => 'Saving Product...', 89 | 'deleting' => 'Deleting Product...', 90 | 'delete_confirm' => 'Do you really want to delete this product?', 91 | 'return_to_products' => 'Return to product list', 92 | ], 93 | 'products' => [ 94 | 'manage' => 'Manage Products', 95 | 'new' => 'New Product', 96 | 'options' => [ 97 | 'label' => 'More Options', 98 | 'forum' => 'Support Forum', 99 | ], 100 | 'enabled' => 'Enabled', 101 | 'visible' => 'Visible', 102 | 'available' => 'Available', 103 | ], 104 | 'settings' => [ 105 | 'currency' => [ 106 | 'label' => 'Currency', 107 | 'description' => 'Set the currency code and symbol used on your shop.', 108 | 'name' => 'Currency name', 109 | 'code' => 'ISO Code', 110 | 'decimals' => 'Decimal places', 111 | 'prefix' => 'Left symbol', 112 | 'suffix' => 'Right symbol', 113 | 'thousand_separator' => 'Thousands separator', 114 | 'decimal_separator' => 'Decimal point', 115 | ], 116 | 'shop' => [ 117 | 'label' => 'Shop Settings', 118 | 'description' => 'Manage various options related to the public-facing side of your shop.', 119 | 'tab' => 'Products', 120 | 'defaultProductImage' => 'Default product image', 121 | ], 122 | ], 123 | ]; 124 | -------------------------------------------------------------------------------- /models/CurrencySettings.php: -------------------------------------------------------------------------------- 1 | 'decimal places', 17 | ]; 18 | 19 | protected $rules = [ 20 | 'decimals' => ['required', 'numeric', 'between:0,4'], 21 | ]; 22 | 23 | /** 24 | * Set default settings from fields.yaml 25 | */ 26 | public function initSettingsData() 27 | { 28 | foreach ($this->getFieldConfig()->fields as $field => $data) { 29 | $this->{$field} = isset($data['default']) ? $data['default'] : ''; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /models/Product.php: -------------------------------------------------------------------------------- 1 | ['required', 'between:4,255'], 31 | 'slug' => [ 32 | 'required', 33 | 'alpha_dash', 34 | 'between:1,255', 35 | 'unique:octoshop_products' 36 | ], 37 | 'price' => ['numeric', 'max:99999999.99', 'min:0'], 38 | ]; 39 | 40 | /** 41 | * @var array Attributes to mutate as dates 42 | */ 43 | protected $dates = ['available_at', 'created_at', 'updated_at']; 44 | 45 | /** 46 | * @var array Image attachments 47 | */ 48 | public $attachMany = [ 49 | 'images' => ['System\Models\File'] 50 | ]; 51 | 52 | protected $urlComponentName = 'shopProduct'; 53 | 54 | public function __construct(array $attributes = []) 55 | { 56 | parent::__construct($attributes); 57 | 58 | $this->setUrlPageName('product.htm'); 59 | } 60 | 61 | public function canBePurchased() 62 | { 63 | if ((bool) $this->attributes['is_enabled'] === false) { 64 | return false; 65 | } 66 | 67 | switch ((int) $this->attributes['is_available']) { 68 | case 2: 69 | $date = $this->attributes['available_at']; 70 | 71 | return Carbon::now()->gt(new Carbon($date)); 72 | case 1: 73 | return true; 74 | default: 75 | return false; 76 | } 77 | } 78 | 79 | public function cannotBePurchased() 80 | { 81 | return !$this->canBePurchased(); 82 | } 83 | 84 | public function isComingSoon() 85 | { 86 | $available = (int) $this->attributes['is_available']; 87 | 88 | if ($available !== 2 || !isset($this->attributes['available_at'])) { 89 | return false; 90 | } 91 | 92 | $date = $this->attributes['available_at']; 93 | 94 | return Carbon::now()->lt(new Carbon($date)); 95 | } 96 | 97 | 98 | ############################################################# 99 | # Search scopes # 100 | ############################################################# 101 | 102 | public function scopeFindBySlug($query, $slug) 103 | { 104 | return $query->whereSlug($slug); 105 | } 106 | 107 | public function scopeEnabled($query) 108 | { 109 | return $query->whereIsEnabled(true); 110 | } 111 | 112 | public function scopeVisible($query) 113 | { 114 | return $query->enabled()->whereIsVisible(true); 115 | } 116 | 117 | public function scopeAvailable($query) 118 | { 119 | return $query->enabled() 120 | ->where('is_available', '>', '0') 121 | ->orWhere('available_at', '<=', Carbon::now()); 122 | } 123 | 124 | 125 | ############################################################# 126 | # Single object getters # 127 | ############################################################# 128 | 129 | public function scopeFirstEnabled($query) 130 | { 131 | return $query->enabled()->firstOrFail(); 132 | } 133 | 134 | public function scopeFirstAvailable($query) 135 | { 136 | return $query->available()->firstOrFail(); 137 | } 138 | 139 | public function scopeFirstWithImages($query) 140 | { 141 | return $query->withImages()->firstOrFail(); 142 | } 143 | 144 | public function scopeFirstEnabledWithImages($query) 145 | { 146 | return $query->enabled()->firstWithImages(); 147 | } 148 | 149 | public function scopeFirstAvailableWithImages($query) 150 | { 151 | return $query->available()->firstWithImages(); 152 | } 153 | 154 | 155 | ############################################################# 156 | # Collection getters # 157 | ############################################################# 158 | 159 | public function scopeAllEnabled($query) 160 | { 161 | return $query->enabled()->get(); 162 | } 163 | 164 | public function scopeAllAvailable($query) 165 | { 166 | return $query->available()->get(); 167 | } 168 | 169 | public function scopeAllWithImages($query) 170 | { 171 | return $query->withImages()->get(); 172 | } 173 | 174 | public function scopeAllEnabledWithImages($query) 175 | { 176 | return $query->enabled()->allWithImages(); 177 | } 178 | 179 | public function scopeAllVisibleWithImages($query) 180 | { 181 | return $query->visible()->allWithImages(); 182 | } 183 | 184 | public function scopeAllAvailableWithImages($query) 185 | { 186 | return $query->available()->allWithImages(); 187 | } 188 | 189 | 190 | ############################################################# 191 | # Eager load helpers # 192 | ############################################################# 193 | 194 | public function scopeWithImages($query) 195 | { 196 | return $query->with(['images' => function ($query) { 197 | $query->orderBy('sort_order', 'asc'); 198 | }]); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /models/ShopSetting.php: -------------------------------------------------------------------------------- 1 | ['System\Models\File'], 15 | ]; 16 | 17 | protected $defaults = []; 18 | 19 | /** 20 | * Set default settings. October doesn't grab them from fields.yaml 21 | */ 22 | public function initSettingsData() 23 | { 24 | foreach ($this->defaults as $default => $value) { 25 | $this->$default = $value; 26 | } 27 | } 28 | 29 | public function registerDefaults(array $defaults) 30 | { 31 | return $this->defaults = array_merge($this->defaults, $defaults); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /models/currencysettings/fields.yaml: -------------------------------------------------------------------------------- 1 | fields: 2 | name: 3 | label: octoshop.core::settings.currency.name 4 | default: Pound sterling 5 | code: 6 | label: octoshop.core::settings.currency.code 7 | span: left 8 | default: GBP 9 | decimals: 10 | label: octoshop.core::settings.currency.decimals 11 | span: right 12 | default: 2 13 | prefix: 14 | label: octoshop.core::settings.currency.prefix 15 | default: £ 16 | span: left 17 | containerAttributes: 18 | style: width:24%; 19 | suffix: 20 | label: octoshop.core::settings.currency.suffix 21 | span: left 22 | containerAttributes: 23 | style: width:24%;clear:none;margin-left:0.5%; 24 | thousand_separator: 25 | label: octoshop.core::settings.currency.thousand_separator 26 | span: right 27 | default: , 28 | containerAttributes: 29 | style: width:24%; 30 | decimal_separator: 31 | label: octoshop.core::settings.currency.decimal_separator 32 | span: right 33 | default: . 34 | containerAttributes: 35 | style: width:24%;clear:none;margin-right:0.5%; 36 | -------------------------------------------------------------------------------- /models/product/columns.yaml: -------------------------------------------------------------------------------- 1 | columns: 2 | id: 3 | label: octoshop.core::lang.product.id 4 | searchable: true 5 | title: 6 | label: octoshop.core::lang.product.title 7 | searchable: true 8 | model: 9 | label: octoshop.core::lang.product.model 10 | searchable: true 11 | invisible: true 12 | slug: 13 | label: octoshop.core::lang.product.slug 14 | searchable: false 15 | available_at: 16 | label: octoshop.core::lang.product.availableAt 17 | searchable: true 18 | type: date 19 | created_at: 20 | label: octoshop.core::lang.product.createdAt 21 | searchable: true 22 | type: date 23 | invisible: true 24 | updated_at: 25 | label: octoshop.core::lang.product.updatedAt 26 | searchable: true 27 | type: date 28 | invisible: true 29 | -------------------------------------------------------------------------------- /models/product/fields.yaml: -------------------------------------------------------------------------------- 1 | fields: 2 | title: 3 | label: octoshop.core::lang.product.title 4 | span: left 5 | placeholder: octoshop.core::lang.product.title_placeholder 6 | required: true 7 | 8 | slug: 9 | label: octoshop.core::lang.product.slug 10 | span: right 11 | required: true 12 | preset: 13 | field: title 14 | type: slug 15 | 16 | toolbar: 17 | type: partial 18 | path: product_toolbar 19 | cssClass: collapse-visible 20 | 21 | secondaryTabs: 22 | defaultTab: octoshop.core::lang.product.tabs.manage 23 | fields: 24 | tagline: 25 | label: octoshop.core::lang.product.tagline 26 | tab: octoshop.core::lang.product.tabs.edit 27 | span: left 28 | placeholder: octoshop.core::lang.product.tagline_placeholder 29 | 30 | model: 31 | label: octoshop.core::lang.product.model 32 | tab: octoshop.core::lang.product.tabs.edit 33 | type: text 34 | span: right 35 | 36 | description: 37 | label: octoshop.core::lang.product.description 38 | tab: octoshop.core::lang.product.tabs.edit 39 | type: richeditor 40 | size: giant 41 | 42 | is_enabled: 43 | label: octoshop.core::lang.product.isEnabled 44 | comment: octoshop.core::lang.product.isEnabled_comment 45 | type: switch 46 | span: left 47 | 48 | is_available: 49 | label: octoshop.core::lang.product.isAvailable 50 | span: right 51 | type: dropdown 52 | default: 1 53 | options: 54 | 0: octoshop.core::lang.product.available 55 | 1: octoshop.core::lang.product.unavailable 56 | 2: octoshop.core::lang.product.coming_soon 57 | 58 | is_visible: 59 | label: octoshop.core::lang.product.isVisible 60 | comment: octoshop.core::lang.product.isVisible_comment 61 | type: switch 62 | default: true 63 | span: left 64 | 65 | available_at: 66 | label: octoshop.core::lang.product.availableAt 67 | type: datepicker 68 | span: right 69 | cssClass: checkbox-align 70 | trigger: 71 | action: show 72 | field: is_available 73 | condition: value[2] 74 | 75 | price: 76 | label: octoshop.core::lang.product.price 77 | comment: octoshop.core::lang.product.price_comment 78 | type: number 79 | span: right 80 | 81 | minimum_qty: 82 | label: octoshop.core::lang.product.minimumQty 83 | type: number 84 | span: right 85 | default: 1 86 | 87 | images: 88 | tab: octoshop.core::lang.product.tabs.images 89 | type: Backend\FormWidgets\FileUpload 90 | mode: image 91 | imageHeight: 120 92 | imageWidth: 120 93 | -------------------------------------------------------------------------------- /models/shopsetting/fields.yaml: -------------------------------------------------------------------------------- 1 | tabs: 2 | fields: 3 | default_product_image: 4 | label: octoshop.core::lang.settings.shop.defaultProductImage 5 | type: fileupload 6 | imageWidth: 200 7 | imageHeight: 200 8 | tab: octoshop.core::lang.settings.shop.tab 9 | -------------------------------------------------------------------------------- /traits/Uuidable.php: -------------------------------------------------------------------------------- 1 | bindEvent('model.beforeCreate', function() use ($model) { 15 | $model->attributes['uuid'] = Uuid::generate()->binary; 16 | }); 17 | 18 | $model->bindEvent('model.beforeUpdate', function() use ($model) { 19 | $originalUuid = $model->getOriginal('uuid'); 20 | 21 | if (is_null($originalUuid)) { 22 | $model->uuid = Uuid::generate()->binary; 23 | } elseif ($originalUuid !== $model->attributes['uuid']) { 24 | $model->uuid = $originalUuid; 25 | } 26 | }); 27 | }); 28 | } 29 | 30 | /** 31 | * Convert the binary UUID to a real Uuid instance 32 | * @return Uuid 33 | */ 34 | public function getUuidAttribute($value) 35 | { 36 | return is_null($value) ? $value : Uuid::import($value); 37 | } 38 | 39 | /** 40 | * Find the current model by a human-readable UUID 41 | * @param string $uuid 42 | */ 43 | public function scopeFindByUuid($query, $uuid) 44 | { 45 | $uuid = Uuid::import($uuid); 46 | 47 | return $query->whereUuid($uuid->binary)->firstOrFail(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /updates/Migration.php: -------------------------------------------------------------------------------- 1 | [ 16 | 'code' => 'Feegleweb.OctoshopLite', 17 | 'namespace' => 'Feegleweb\OctoshopLite', 18 | ], 19 | 'full' => [ 20 | 'code' => 'Feegleweb.Octoshop', 21 | 'namespace' => 'Feegleweb\Octoshop', 22 | ], 23 | ]; 24 | 25 | public function __construct() 26 | { 27 | $this->pluginManager = PluginManager::instance(); 28 | } 29 | 30 | public function upgradingFrom($version) 31 | { 32 | // Returns true even if disabled 33 | return $this->pluginManager->hasPlugin($this->resolvePluginNamespace($version)); 34 | } 35 | 36 | public function disablePlugin($version) 37 | { 38 | $code = $this->resolvePluginCode($version); 39 | 40 | // Only returns true if installed AND enabled 41 | if ($this->pluginManager->exists($code)) { 42 | $this->pluginManager->disablePlugin($code, true); 43 | } 44 | } 45 | 46 | public function purgePlugin($version) 47 | { 48 | $code = $this->resolvePluginCode($version); 49 | 50 | if ($path = $this->pluginManager->getPluginPath($code)) { 51 | File::deleteDirectory($path); 52 | } 53 | 54 | VersionManager::instance()->purgePlugin($code); 55 | } 56 | 57 | public function resolvePluginCode($version) 58 | { 59 | $code = in_array($version, $this->versionMap) ? $this->versionMap[$version]['code'] : null; 60 | 61 | if (!$code) { 62 | throw new ApplicationException(Lang::get('octoshop.core::lang.migration.invalid_version')); 63 | } 64 | 65 | return $code; 66 | } 67 | 68 | public function resolvePluginNamespace($version) 69 | { 70 | $namespace = in_array($version, $this->versionMap) ? $this->versionMap[$version]['namespace'] : null; 71 | 72 | if (!$namespace) { 73 | throw new ApplicationException(Lang::get('octoshop.core::lang.migration.invalid_version')); 74 | } 75 | 76 | return $namespace; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /updates/create_products_table.php: -------------------------------------------------------------------------------- 1 | engine = 'InnoDB'; 11 | $table->increments('id'); 12 | $table->string('title')->index(); 13 | $table->string('slug')->index()->unique(); 14 | $table->string('tagline')->nullable(); 15 | $table->string('model')->nullable(); 16 | $table->longText('description'); 17 | $table->boolean('is_enabled')->default(false); 18 | $table->tinyInteger('is_available')->default(1); 19 | $table->boolean('is_visible')->default(true); 20 | $table->dateTime('available_at')->nullable(); 21 | $table->decimal('price', 20, 5)->default(0)->nullable(); 22 | $table->integer('minimum_qty')->default(1); 23 | $table->timestamps(); 24 | }); 25 | } 26 | 27 | public function down() 28 | { 29 | Schema::dropIfExists('octoshop_products'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /updates/version.yaml: -------------------------------------------------------------------------------- 1 | 2.0.0: 2 | - Birth of Octoshop Core 3 | - create_products_table.php 4 | -------------------------------------------------------------------------------- /util/Currency.php: -------------------------------------------------------------------------------- 1 | decimals, 14 | $currency->decimal_separator, 15 | $currency->thousand_separator 16 | ); 17 | 18 | return htmlentities($currency->prefix.$price.$currency->suffix); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /util/Image.php: -------------------------------------------------------------------------------- 1 | 100) { 77 | $quality = self::IMG_DEFAULT_QUALITY; 78 | } 79 | 80 | if (!$image && !($image = ShopSetting::instance()->default_product_image)) { 81 | $dimensions = $width.'x'.($height ?: $width); 82 | $format = $extension == 'auto' ? 'png' : $extension; 83 | 84 | return "https://octoshop.co/api/placeholder/{$dimensions}.{$format}"; 85 | } 86 | 87 | return $image->getThumb($width, $height ?: $width, compact('mode', 'extension', 'quality')); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /util/UrlMaker.php: -------------------------------------------------------------------------------- 1 | url === null) { 20 | $this->url = $this->makeUrl(); 21 | } 22 | 23 | return $this->url; 24 | } 25 | 26 | public function setUrlAttribute($value) 27 | { 28 | $this->url = $value; 29 | } 30 | 31 | public function setUrlPageName($pageName) 32 | { 33 | static::$urlPageName = $pageName; 34 | } 35 | 36 | public function getUrlPageName() 37 | { 38 | if (static::$urlPageName !== null) { 39 | return static::$urlPageName; 40 | } 41 | 42 | /* 43 | * Cache 44 | */ 45 | $key = 'urlMaker'.$this->urlComponentName.crc32(get_class($this)); 46 | 47 | $cached = Cache::get($key, false); 48 | if ($cached !== false && ($cached = @unserialize($cached)) !== false) { 49 | $filePath = array_get($cached, 'path'); 50 | $mtime = array_get($cached, 'mtime'); 51 | if (!File::isFile($filePath) || ($mtime != File::lastModified($filePath))) { 52 | $cached = false; 53 | } 54 | } 55 | 56 | if ($cached !== false) { 57 | return static::$urlPageName = array_get($cached, 'fileName'); 58 | } 59 | 60 | $page = Page::whereComponent($this->urlComponentName, 'isPrimary', '1')->first(); 61 | 62 | if (!$page) { 63 | throw new ApplicationException(sprintf( 64 | Lang::get('octoshop.core::lang.urlmaker.no_primary_component'), 65 | $this->urlComponentName, 66 | get_class($this) 67 | )); 68 | } 69 | 70 | $baseFileName = $page->getBaseFileName(); 71 | $filePath = $page->getFilePath(); 72 | 73 | $cached = [ 74 | 'path' => $filePath, 75 | 'fileName' => $baseFileName, 76 | 'mtime' => @File::lastModified($filePath) 77 | ]; 78 | 79 | Cache::put($key, serialize($cached), Config::get('cms.parsedPageCacheTTL', 1440)); 80 | 81 | return static::$urlPageName = $baseFileName; 82 | } 83 | 84 | /** 85 | * Returns an array of values to use in URL generation. 86 | * @return @array 87 | */ 88 | public function getUrlParams() 89 | { 90 | return [ 91 | 'id' => $this->id, 92 | 'slug' => $this->slug, 93 | ]; 94 | } 95 | 96 | protected function makeUrl() 97 | { 98 | $controller = Controller::getController() ?: new Controller; 99 | 100 | return $controller->pageUrl($this->getUrlPageName(), $this->getUrlParams()); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /util/Uuid.php: -------------------------------------------------------------------------------- 1 | binary = $uuid; 36 | 37 | $this->string = implode('-', [ 38 | bin2hex(substr($uuid, 0, 4)), 39 | bin2hex(substr($uuid, 4, 2)), 40 | bin2hex(substr($uuid, 6, 2)), 41 | bin2hex(substr($uuid, 8, 2)), 42 | bin2hex(substr($uuid, 10, 6)) 43 | ]); 44 | } 45 | 46 | /** 47 | * Create a new Version 4 UUID 48 | * @return Uuid 49 | */ 50 | public static function generate() 51 | { 52 | $uuid = static::randomBytes(); 53 | 54 | // Set variant 55 | $uuid[8] = chr(ord($uuid[8]) & static::CLEAR_VAR | static::VAR_RFC); 56 | 57 | // Set version 58 | $uuid[6] = chr(ord($uuid[6]) & static::CLEAR_VER | static::VERSION_4); 59 | 60 | return new static($uuid); 61 | } 62 | 63 | /** 64 | * Import an existing UUID 65 | * @param mixed $uuid UUID as either binary, string or a Uuid instance. 66 | * @return Uuid 67 | */ 68 | public static function import($uuid) 69 | { 70 | if ($uuid instanceof self) { 71 | return $uuid; 72 | } 73 | 74 | if (strlen($uuid) === 36) { 75 | $uuid = hex2bin(str_replace('-', '', $uuid)); 76 | } 77 | 78 | return new static($uuid); 79 | } 80 | 81 | /** 82 | * Generate random bytes for the UUID 83 | * @param $bytes 84 | * @return string 85 | */ 86 | protected static function randomBytes($bytes = 16) 87 | { 88 | if (function_exists('random_bytes')) { 89 | return random_bytes($bytes); 90 | } 91 | 92 | if (function_exists('openssl_random_pseudo_bytes')) { 93 | return openssl_random_pseudo_bytes($bytes); 94 | } 95 | 96 | if (function_exists('mcrypt_encrypt')) { 97 | return mcrypt_create_iv($bytes, MCRYPT_DEV_URANDOM); 98 | } 99 | 100 | throw new SystemException("You must be running PHP 7, or an older version with Mcrypt or OpenSSL."); 101 | } 102 | 103 | /** 104 | * Get the UUID's string representation. 105 | * @return string 106 | */ 107 | public function __toString() 108 | { 109 | return $this->string; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /vagrant/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "octoshop.dev" > /etc/hostname 4 | echo "127.0.0.1 octoshop.dev dev localhost" > /etc/hosts 5 | 6 | export DEBIAN_FRONTEND=noninteractive 7 | 8 | apt-get update -q && apt-get install -qy git unzip zsh 9 | chsh -s /bin/zsh && chsh -s /bin/zsh ubuntu 10 | 11 | debconf-set-selections <<< 'mysql-server mysql-server/root_password password root' 12 | debconf-set-selections <<< 'mysql-server mysql-server/root_password_again password root' 13 | 14 | apt-get install -qy mysql-server mysql-client nginx php-fpm 15 | 16 | echo "CREATE USER 'octoshop'@'localhost' IDENTIFIED BY 'octoshop'" | mysql -u root -proot 17 | echo "CREATE DATABASE octoshop" | mysql -u root -proot 18 | echo "GRANT ALL ON octoshop.* TO 'octoshop'@'localhost'" | mysql -u root -proot 19 | echo "FLUSH PRIVILEGES" | mysql -u root -proot 20 | 21 | apt-get install -qy php-mysql php-mbstring php-curl php-gd php-intl php-mcrypt php-xml php-zip 22 | 23 | cat >> /etc/nginx/sites-available/default <