diff --git a/database/migrations/2024_05_31_103315_create_cart_items_table.php b/database/migrations/2024_05_31_103315_create_cart_items_table.php index 9a1d272..5839076 100644 --- a/database/migrations/2024_05_31_103315_create_cart_items_table.php +++ b/database/migrations/2024_05_31_103315_create_cart_items_table.php @@ -21,6 +21,7 @@ public function up(): void $table->foreignId($cartForeignName)->constrained($cartTableName)->cascadeOnDelete(); $table->morphs('itemable'); $table->unsignedInteger('quantity')->default(1); + $table->json('options')->nullable(); $table->timestamps(); }); diff --git a/src/Drivers/LaravelCartDatabase.php b/src/Drivers/LaravelCartDatabase.php index 4c51074..bba5728 100644 --- a/src/Drivers/LaravelCartDatabase.php +++ b/src/Drivers/LaravelCartDatabase.php @@ -3,6 +3,7 @@ namespace Binafy\LaravelCart\Drivers; use Binafy\LaravelCart\Models\Cart; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class LaravelCartDatabase implements Driver @@ -86,6 +87,62 @@ public function emptyCart(?int $userId = null): static return $this; } + /** + * Get option for item. + */ + public function getOption(string $option, ?int $itemId = null, ?int $userId = null): mixed + { + $cart = Cart::query()->firstOrCreate(['user_id' => $this->resolveUserId($userId)]); + $items = $cart->items()->when(! is_null($itemId), function (Builder $builder) use ($itemId) { + $builder->where('id', $itemId); + }); + + return $items->first()->getOption($option); + } + + /** + * Get all options of one item. + */ + public function getOptions(?int $itemId = null, ?int $userId = null): mixed + { + $cart = Cart::query()->firstOrCreate(['user_id' => $this->resolveUserId($userId)]); + $items = $cart->items()->when(! is_null($itemId), function (Builder $builder) use ($itemId) { + $builder->where('id', $itemId); + }); + + return $items->first()->getOptions(); + } + + /** + * Set option for item. + */ + public function setOption(string $key, mixed $value, ?int $itemId = null, ?int $userId = null): static + { + $cart = Cart::query()->firstOrCreate(['user_id' => $this->resolveUserId($userId)]); + $items = $cart->items()->when(! is_null($itemId), function (Builder $builder) use ($itemId) { + $builder->where('id', $itemId); + }); + + $items->first()->setOption($key, $value); + + return $this; + } + + /** + * Get option for item. + */ + public function addOption(string $key, mixed $value, ?int $itemId = null, ?int $userId = null): static + { + $cart = Cart::query()->firstOrCreate(['user_id' => $this->resolveUserId($userId)]); + $items = $cart->items()->when(! is_null($itemId), function (Builder $builder) use ($itemId) { + $builder->where('id', $itemId); + }); + + $items->first()->addOption($key, $value); + + return $this; + } + /** * Resolve the user ID, defaulting to the authenticated user. */ diff --git a/src/Drivers/LaravelCartSession.php b/src/Drivers/LaravelCartSession.php index a3e1ad9..e0f232c 100644 --- a/src/Drivers/LaravelCartSession.php +++ b/src/Drivers/LaravelCartSession.php @@ -142,21 +142,4 @@ protected function resolveUserId(?int $userId): int { return $userId ?? auth()->id(); } - - /** - * Format an item for storage. - */ - protected function formatItem(Model|array $item): array - { - if ($item instanceof Model) { - return [ - 'id' => $item->getKey(), - 'type' => get_class($item), - 'quantity' => 1, - 'attributes' => $item->toArray(), - ]; - } - - return $item; - } } diff --git a/src/LaravelCart.php b/src/LaravelCart.php index 2b0b7dc..faff20d 100644 --- a/src/LaravelCart.php +++ b/src/LaravelCart.php @@ -2,8 +2,6 @@ namespace Binafy\LaravelCart; -use Illuminate\Support\Facades\Facade; - /** * @method static \Binafy\LaravelCart\Drivers\Driver driver(string|null $driver = null) * @method static \Binafy\LaravelCart\Drivers\Driver storeItem(\Illuminate\Database\Eloquent\Model|array $item, int|null $userId = null) @@ -12,12 +10,16 @@ * @method static \Binafy\LaravelCart\Drivers\Driver decreaseQuantity(\Illuminate\Database\Eloquent\Model $item, int $quantity = 1) * @method static \Binafy\LaravelCart\Drivers\Driver removeItem(\Illuminate\Database\Eloquent\Model $item) * @method static \Binafy\LaravelCart\Drivers\Driver emptyCart() + * @method static \Binafy\LaravelCart\Drivers\Driver getOption(string $option, ?int $itemId = null, ?int $userId = null) + * @method static \Binafy\LaravelCart\Drivers\Driver getOptions(?int $itemId = null, ?int $userId = null) + * @method static \Binafy\LaravelCart\Drivers\Driver setOption(string $key, mixed $value, ?int $itemId = null, ?int $userId = null) + * @method static \Binafy\LaravelCart\Drivers\Driver addOption(string $key, mixed $value, ?int $itemId = null, ?int $userId = null) * @method static string getDefaultDriver() * @method static void setDefaultDriver(string $name) * * @see \Binafy\LaravelCart\Manager\LaravelCartManager */ -class LaravelCart extends Facade +class LaravelCart extends \Illuminate\Support\Facades\Facade { /** * Get the registered name of the component. diff --git a/src/Models/CartItem.php b/src/Models/CartItem.php index 5ed65f3..21ca6ee 100644 --- a/src/Models/CartItem.php +++ b/src/Models/CartItem.php @@ -14,7 +14,7 @@ class CartItem extends Model * * @var string[] */ - protected $fillable = ['cart_id', 'itemable_id', 'itemable_type', 'quantity']; + protected $guarded = ['id']; /** * Create a new instance of the model. @@ -26,6 +26,56 @@ public function __construct(array $attributes = []) $this->table = config('laravel-cart.cart_items.table', 'cart_items'); } + // Methods + + /** + * Get option. + */ + public function getOption(string $option): mixed + { + $options = json_decode($this->options, true); + + return $options[$option] ?? null; + } + + /** + * Get options. + */ + public function getOptions(): mixed + { + return json_decode($this->options, true); + } + + /** + * Set option. + */ + public function setOption(string $key, mixed $value): static + { + $this->update(['options' => json_encode([$key => $value])]); + + return $this; + } + + /** + * Add option. + */ + public function addOption(string $key, mixed $value): static + { + $options = $this->getOptions(); + if (! is_array($options)) { + $options = json_decode($options, true); + } + + $options[$key] = $value; + + $this->options = json_encode($options); + $this->save(); + + return $this; + } + + // Relations + /** * Relation polymorphic, inverse one-to-one or many relationship. */ diff --git a/tests/Feature/Manager/Database/CartItemOptionTest.php b/tests/Feature/Manager/Database/CartItemOptionTest.php new file mode 100644 index 0000000..1ab6885 --- /dev/null +++ b/tests/Feature/Manager/Database/CartItemOptionTest.php @@ -0,0 +1,102 @@ +create(['name' => 'Milwad', 'email' => 'milwad.dev@gmail.com']); + auth()->login($user); + $product1 = Product::query()->create(['title' => 'Product 1']); + + // Store items to cart + $cart = Cart::query()->firstOrCreate(['user_id' => $user->id]); + $cart->storeItem($product1); + + // Set options + LaravelCart::driver('database')->setOption('size', 34); + + // Assertions + expect(LaravelCart::driver('database')->getOption('size'))->toBe(34); + + // DB Assertions + assertDatabaseCount('cart_items', 1); +}); + +test('can set option for cart with specific id', closure: function () { + $user = User::query()->create(['name' => 'Milwad', 'email' => 'milwad.dev@gmail.com']); + auth()->login($user); + $product1 = Product::query()->create(['title' => 'Product 1']); + $product2 = Product::query()->create(['title' => 'Product 2']); + + // Store items to cart + $cart = Cart::query()->firstOrCreate(['user_id' => $user->id]); + $cart->storeItem($product1); + $cart->storeItem($product2); + + // Set options + LaravelCart::driver('database')->setOption('size', 80, 2); + + // Assertions + expect(LaravelCart::driver('database')->getOption('size', 2)) + ->toBe(80) + ->and(LaravelCart::driver('database')->getOptions(2)) + ->toBe(['size' => 80]); + + // Add option + LaravelCart::driver('database')->addOption('address', 'something', 2); + + // DB Assertions + assertDatabaseCount('cart_items', 2); +}); + +test('can get all option of one cart item', closure: function () { + $user = User::query()->create(['name' => 'Milwad', 'email' => 'milwad.dev@gmail.com']); + auth()->login($user); + $product1 = Product::query()->create(['title' => 'Product 1']); + + // Store items to cart + $cart = Cart::query()->firstOrCreate(['user_id' => $user->id]); + $cart->storeItem($product1); + + // Set options + LaravelCart::driver('database')->setOption('size', 34); + + // Assertions + expect(LaravelCart::driver('database')->getOptions())->toBe(['size' => 34]); + + // DB Assertions + assertDatabaseCount('cart_items', 1); +}); + +test('can add option for one cart item', closure: function () { + $user = User::query()->create(['name' => 'Milwad', 'email' => 'milwad.dev@gmail.com']); + auth()->login($user); + $product1 = Product::query()->create(['title' => 'Product 1']); + + // Store items to cart + $cart = Cart::query()->firstOrCreate(['user_id' => $user->id]); + $cart->storeItem($product1); + + // Set options + LaravelCart::driver('database')->addOption('size', 34); + LaravelCart::driver('database')->addOption('address', 'something'); + + // Assertions + expect(LaravelCart::driver('database')->getOption('size')) + ->toBe(34) + ->and(LaravelCart::driver('database')->getOption('address')) + ->toBe('something'); + + // DB Assertions + assertDatabaseCount('cart_items', 1); +}); diff --git a/tests/Feature/Manager/LaravelCartManagerTest.php b/tests/Feature/Manager/LaravelCartManagerTest.php new file mode 100644 index 0000000..a8f3871 --- /dev/null +++ b/tests/Feature/Manager/LaravelCartManagerTest.php @@ -0,0 +1,10 @@ +toBe(config('laravel-cart.driver.default')); + + config(['laravel-cart.driver.default' => 'session']); + expect(LaravelCart::getDefaultDriver())->toBe('session'); +}); diff --git a/tests/Feature/Models/CartItemOptionTest.php b/tests/Feature/Models/CartItemOptionTest.php new file mode 100644 index 0000000..5c91eb4 --- /dev/null +++ b/tests/Feature/Models/CartItemOptionTest.php @@ -0,0 +1,71 @@ +create(['name' => 'Milwad', 'email' => 'milwad.dev@gmail.comd']); + $product1 = Product::query()->create(['title' => 'Product 1']); + + // Store items to cart + $cart = Cart::query()->firstOrCreate(['user_id' => $user->id]); + $cart->storeItem($product1); + + // Set options + $cart->items()->first()->setOption('size', 34); + + // Assertions + expect($cart->items()->first()->getOption('size'))->toBe(34); + + // DB Assertions + assertDatabaseCount('cart_items', 1); +}); + +test('can get all option of one cart item', closure: function () { + $user = User::query()->create(['name' => 'Milwad', 'email' => 'milwad.dev@gmail.comd']); + $product1 = Product::query()->create(['title' => 'Product 1']); + + // Store items to cart + $cart = Cart::query()->firstOrCreate(['user_id' => $user->id]); + $cart->storeItem($product1); + + // Set options + $cart->items()->first()->setOption('size', 34); + + // Assertions + expect($cart->items()->first()->getOptions())->toBe(['size' => 34]); + + // DB Assertions + assertDatabaseCount('cart_items', 1); +}); + +test('can add option for one cart item', closure: function () { + $user = User::query()->create(['name' => 'Milwad', 'email' => 'milwad.dev@gmail.comd']); + $product1 = Product::query()->create(['title' => 'Product 1']); + + // Store items to cart + $cart = Cart::query()->firstOrCreate(['user_id' => $user->id]); + $cart->storeItem($product1); + + // Set options + $cart->items()->first()->addOption('size', 34); + $cart->items()->first()->addOption('address', 'something'); + + // Assertions + expect($cart->items()->first()->getOption('size')) + ->toBe(34) + ->and($cart->items()->first()->getOption('address')) + ->toBe('something'); + + // DB Assertions + assertDatabaseCount('cart_items', 1); +});