conedevelopment / bazar

Bazar is an e-commerce package for Laravel applications.
https://root.conedevelopment.com
MIT License
418 stars 56 forks source link

[Question] Using variants for configurable options #81

Closed dd10-e closed 3 years ago

dd10-e commented 3 years ago

Hello !

I wonder how it is possible to use Variants database structure to do configurable products like :

I'm thinking more at Tesla website or a window configurator (this one seems to do pretty good things : https://fensternorm.com/en/configurator/windows/upvc/trocal-70-eco/c1)

Pretty hard !

I've done some research with the current implementation, and it was wonderful. I really liked the way you did this, simple and clean API.

The first thing I need to do is to add some attributes (something like a description and step order) and prices to this option, like you're doing on Variants. I can maybe add some attributes on options columns since it's a JSON column, but this might not be a very clean way to do this because I will lose all prices and stock handling of variants, and i don't think i can handle all this things listed above.

So I've considered to requested directly variations this way :

$variants = $product->variantsWhere($this->selection);
// $this->selection is an array of options, handle the same way as variants method of instance Product but return me all available Variants
// Example : $variants = $product->variantsWhere(["Size" => "A", "Color" => "*", "Dimension" => "*"]);
//  "*" is when the User haven't yet selected an option, so this line will give me all Variants with `Size === "A"`

Implementation look like this (unfinished) :

    public function variantsWhere(array $option)
    {
        return $this->variants()->where(function ($query) use ($option) {

            foreach ($option as $key => $value) {
                if ($value !== "*") {
                    $query->where('option->' . $key, $value);
                }
            }

            return $query;
        })->get();
    }

This way, I can add more attributes and have pricing, but I wouldn't be able to query : "Select me all variants when there is Size to XS selected" for example.

I think the best way to do this, and for having the more flexibility should be to have a separate table that we can name configurations with columns option_name|string, value|string, description|string, prices|json, inventory|json, product_id.

Data will look this way (note that i've added some extra columns for illustrate application logic used by the application after Bazar Framework) :

option_name value description       price inventory product_id available_on_option_name next_step order
Size         XS     Little           10%                         All                       Color     1    
Size         S     A bit moe bigger 10                           All                       Color     1    
Size         L     link             100                         All                       Color     1    
Color       red                     10%                         Size                     Dimension 2    
Color       blue                     10                           Size                     Dimension 2    
Color       green                   100                         Size                     Dimension 2    

Using another table should be more flexible, right ? But it really looks like variations in fact, except that we don't use the options column of products for query variations. In the other side configurations will be query using $product->configurations()->where('order', 1)->andGetMeArrayValuesWithStepName().

Not tested at all ! But it should be more flexible. I think I described the idea correctly.

So the response seems to be "No.".  But I wanted to share my reflection and ask if it's something that should be added on Bazar Framework ? I'm mainly curious about the roadmap where you mention More flexible option/property management. Are you thinking of something similar or another way ?

Perfect occasion to say that you did an amazing clean work. I learn a lot !

Pretty big question ! Thank you for reading and feel free to close if it's out of the scope.

iamgergo commented 3 years ago

Hi @dd10-e,

Thank you for this detailed issue, it makes complete sense! Reworking the options and variants is on the table for a while, but we always had a more urgent fix, feature to do.

I really like the way you described the problem, I understand this might be a real need for others as well. Hopefully, in v0.5 or v0.6 we can work on this and find an implementation that works well.

I keep this issue open until we can turn on this task, so this can provide a good base idea.

Thanks again, we really appreciate it!

vanthao03596 commented 3 years ago

I think we need table options and pivot option_variant table. Each variant is combine of option.

iamgergo commented 3 years ago

Hey @dd10-e,

We've released v0.5, so I think it's time to turn on this one. I'm collecting some resources based on your issue and trying to find a simple and flexible solution. Hopefully, we can manage to implement this.

I'll create a new branch for this feature, so we can target any further related discussion there.

Thanks for your patience!

dd10-e commented 3 years ago

The first step for managing to doing this, is to adding two new concepts into Bazar :

In a user view it's the configuration with a name, description, some useful infos, etc... For example, a color or product specific attributes that we can customize, telling what is the best for you. 

System76 is doing it beautifully : https://system76.com/laptops/galp5/configure

The goal is the same as variants, but in Bazar, a "Variant" is lazy loaded. A variant is fetched depending on some options like ['Color' => 'Blue'] and ['Width' => '200' belonging to a product. Bazar have OptionGroup like a "Color", and OptionsValues for values depanding of an OptionGroup.

So in fact ConfigurationStep is like an attempt to do the same but with more customization. I will show some work I've done experimenting in this.

My goal here is to be able adding a maximum of aspects of a product that we can sell with explanations, descriptions, calculations, this option is more expensive,you can't select this color with this blue, it does not go together, etc...


ConfigurationStep works the same way as Category. For now I just added a field into bazar_categories :

$table->string('type')->default('market'); // can be `market` (visible on market website) or `configuration_step` for queries

Adding this allowed me to separate real categories and configurations that are groups of custom variants.

A little note for this :

I was thinking we can maybe doing like Wordpress taxonomies. But it might me less performant and more complex... In any case,  I should probably use 2 tables, `bazar_categories` and `bazar_configuration_steps`.

Where it started to be more interesting is that I found it more easy and flexible to use Product for Configuration.  For example, a color is a ConfigurationStep having some Configurations like yellow and blue. 

This way I'm able to set a lot more data like a name, images, prices, variants, etc... 

For example, if I select blue. I want to show some variants of the blue like dark or light in a dropdown for example. Product already handles that for us ! Instead of using extending product's options as I was initially thinking of.

For doing this, I added fields into products for knowing if a product is configurable or not : 

// Table : bazar_product
$table->boolean('is_configuration')->default(false); // True if the product will be in a configuration step
$table->boolean('is_configurable')->default(false); // True if the product has some configuration step

If yes, this product can be INSERTED in a  ConfigurationStep. Otherwise it can HAVE some ConfigurationStep. This way I can create a product called "Blue" shown in a ConfigurationStep called "Color". The product "Configure that awesome product" will have "Choose your color" ConfigurationStep with a blue as Configuration.

With that, I want know be able to adding more stuff !

For example, a step called "Choose a dimension", or allowing me to select multiple choices or only one, or even select this step in a popup because I know they will have a lot of options, etc.

For doing this, I added a field (in bazar_product but that's only used if the type is configuration_step), allowing me to change dynamically how data is shown and fetched :

$table->string('selection_type')->nullable(); // Can be for example `dimensions` or `unique_selection` or even `popup_selection`

Below, an example of how I've used a configuration asking for dimensions.  It uses some Laravel Livewire and Blade components with TailwindCSS, in addition to Bazar. (that's really nice stuff!) 

@props([
    'product' => null,
    'configuration' => null,
])

@php
    $quantity = $product->inventory->quantity < 10 ? $product->inventory->quantity : 10;
    $isSelectedProductIdExist = isset($this->configuration[$configuration->name]['selected']);
    $selectedProductId = $isSelectedProductIdExist? $this->configuration[$configuration->name]['selected'] : null;
@endphp

<div class="bg-white">
    <div class="space-y-4">
        <div class="max-w-sm space-y-2">
            @foreach ($product->options as $option => $values)
                <div>
                    <label for="height" class="block text-sm font-normal leading-5 text-gray-700">
                        <span class="font-semibold">{{ $option }}</span> 
                            between 
                            <span class="font-medium">{{ $product->options[$option][array_key_first($product->options[$option])] }}</span> 
                            and 
                            <span class="font-medium">{{ $product->options[$option][array_key_last($product->options[$option])] }}</span> 
                            ({{ config('bazar.dimension_unit') }})
                    </label>
                    <div class="mt-1 relative rounded-md shadow-sm">
                        <input 
                            wire:model.lazy="configuration.{{ $configuration->name }}.{{ $option }}.{{ $product->slug }}" 
                            type="text" 
                            class="shadow-sm focus:ring-red-500 focus:border-red-500 block w-full sm:text-sm border-gray-300 rounded-md" 
                            placeholder="{{ $product->options[$option][array_key_last($product->options[$option])] }}"
                        >
                    </div>
                </div>
            @endforeach

            <button 
                wire:click="chooseDimensionProduct('{{$configuration->name}}', '{{ $product->slug }}')" 
                type="button" 
                class="inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-white bg-red-600 hover:bg-red-500 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500"
            >
                Choose dimensions
            </button>
        </div>

        @error('dimension_' . $configuration->name) 
            <div class="mt-2 text-red-600">{{ $message }}</div> 
        @enderror
    </div>
</div>

For dimensions I wasn't displaying name or description, I've mainly used Variants of Bazar. See $product->options.  In this case, the Configuration had 2 variants height and width because internally it is a product as described before.  I customized the query in chooseDimensionProduct for selecting only superior or equal dimensions than selected by users and some extra stuff.


Writing is really interesting, writing this post just make me realize about how configurations and variants can be unify. Basically, i've did the same as variants but data is structured differently (similar of what @vanthao03596 described) . Anyway, I need to continue working on it for leaning more...

I'm very far from Tesla or System76 configurators. It missing 2 crucials features :

I'm not talking about how it can be elegantly included in carts...

So I have a lot more to do. 

But the good news is that is a lot of fun to work with Bazar, and for now I'm pretty happy because I feel it's a good start a lot more clean way that I in the past. It's very pleasant to use your package.

I'm not 100% convinced it will be useful  for you because I haven't been very far for now. But wanted to let you know I saw your message @iamgergo and thank you !

iamgergo commented 3 years ago

Hey @dd10-e,

Returning here, after a long time!

So, I prepared the next release, which contains some rework of the current option/variant management.

I was struggling with how to achieve a simple solution, and I decide I'll make that in two steps – because PR #124 became a bit bigger than originally planned...

As the first step, I decided to rename the attributes: $product->options now is $product->properties, $variant->option now is $variant->variation. Names in migrations and front-end changed as well.

Also, I flattened the $item->properties attribute, now every property is on the same level, variable properties are not wrapped in the option array.

// Old

{
    "options": {
         "Size": "L",
         "Material": "gold"
    },
    "Custom Text": "This is another prop"
}

// New

{

    "Size": "L",
    "Material": "gold",
    "Custom Text": "This is another prop"
}

With this structure – and with some custom property classes – we'll be able to make validation rules and conditional values easier, as a next step.

What I'm planning now, is having a Property class that holds some basic information about the property – for example, Size or Material, and some custom resolution logic based on the Product, the Item, and the Cart/Order models.

These properties could be registered on the Product model, for example, so when a product is added to the cart if the Item model contains a property that is registered on the product, we call the resolution logic, validation, etc.

use Bazar\Models\Product;
use Bazar\Properties\Size;

Product::resolvePropertyUsing('size', Size::class);

It's very similar to what we have with the Item::resolvePropertyUsing, but instead of a callable, we pass a class, so we have much more flexibility.

My problem is, the front-end basically can be anything, so it's hard to find a convenient way to make this solution easy to use and widely compatible with any front-store solution.

Hopefully, soon we can nail it!

Thanks!