Contents

Managing Multiple Stores from Single Application – Part 2

Managing Multiple Stores

Building the Application

In the first part of the article we mentioned that Laravel 5 will be used to implement our application, so let’s get started!

The centralized application will utilize external stores’ APIs in order to create products. As mentioned earlier in part 1 of the article, implementing a fully operational solution would be quite complex (because of different functionality and database structure of each e-commerce platform) so for the purpose of this article we will only focus on creating products in WooCommerce (API v3) and Magento (version 2.3) based stores.

Let’s start by creating a migration for our Store model that will contain the list of all stores we want to manage:

// database/migrations/2019_02_01_0000001_create_stores_table.php

 

class CreateStoresTable extends Migration

{

    public function up()

{

        Schema::create('stores', function (Blueprint $table) {

              $table->increments('id');

                 $table->string('name', 191);

                 $table->string('base_url', 191)->unique();

                 $table->tinyInteger('type_id')->unsigned();

           });

}

}

Now we are going to need the Store model:

// app/Store.php

 

namespace App;

 

use Illuminate\Database\Eloquent\Model;

 

final class Store extends Model

{

   const TYPE_WOOCOMMERCE = 1;

   const TYPE_MAGENTO = 2;

   

   protected $fillable = [

       'name', 'base_url', 'type_id'

   ];

   

   public $timestamps = false;

}

As mentioned earlier, we’re going to work with 2 stores:

// database/seeds/StoresTableSeeder.php

 

use App\Store;

use Illuminate\Database\Seeder;

 

final class StoresTableSeeder extends Seeder

{

   protected function stores()

   {

       return [

           [

               'name' => 'Demo WooCommerce Store',

               'base_url' => 'http://woocommerce.local/',

               'type_id' => Store::TYPE_WOOCOMMERCE

           ],

           [

               'name' => 'Demo Magento Store',

               'base_url' => 'https://magento.local/',

               'type_id' => Store::TYPE_MAGENTO

           ]

       ];

   }

   

   public function run()

   {

       foreach ($this->stores() as $store) {

           Store::firstOrCreate($store);

       }

   }

}

For security reasons, we are not going to store any sensitive information used for API authentication and authorization purposes (i.e., consumer keys, consumer secrets, usernames, and passwords) in the database. All such data will be stored in the .env file:

// .env

 

STORE_1_CK=ck_e7198a3ce2bd156cfd0ef90b27e51eca0e2b03e2

STORE_1_CS=cs_04757b65516755773991a6f32b13257be3f5fc84

 

STORE_2_USERNAME=test

STORE_2_PASSWORD=abc123

Now that we have our store-related code in place, we can continue with coding the product part. Product model:

// app/Product.php

 

namespace App;

 

final class Product extends Model

{

   protected $fillable = [

       'name', 'sku', 'price', 'status'

   ];

}

Migration for Product model:

// database/migrations/2019_01_01_0000002_create_products_table.php

 

class CreateProductsTable extends Migration

{

    public function up()

{

        Schema::create('products', function (Blueprint $table) {

              $table->increments('id');

               $table->string('name', 191)->unique();

               $table->string('sku', 64)->unique();

               $table->decimal('price', 10, 2);

               $table->decimal('weight', 7, 2);

               $table->tinyInteger('status')->unsigned();

               $table->timestamps();

               $table->softDeletes();

           });

}

}

As you can see the database structure for the products table has been greatly simplified for demonstration purposes but can certainly be extended to cover schema common to all e-commerce platforms. Every time a product is created in the application, it will automatically create the product in all stores defined in the system. We will accomplish this using Laravel’s observer class:

// app/Observers/ProductObserver.php

 

declare(strict_types = 1);

 

namespace App\Observers;

class ProductObserver
{
    public function created(Product $product) {

// @todo

}
}

Let’s register our ProductObserver in AppServiceProvider class:

// app/Providers/AppServiceProvider.php

namespace App\Providers;

 

class AppServiceProvider extends ServiceProvider

{

    public function boot()

{

Product::observe(ProductObserver::class);

}

}

We are still missing the implementation for the ProductObserver class methods, but before we get started on this, we will utilize the factory and adapter design patterns to handle managing products with multiple APIs:

// app/Contracts/Store/Product.php

declare(strict_types = 1);

 

namespace App\Contracts\Store;

 

interface Product

{

   public function createProduct(\App\Product $product): int;

}

Our WooCommerce-based API will be implemented as follows:

// app/Services/Store/WooCommerce.php

declare(strict_types = 1);

 

namespace App\Services\Store;

 

class WooCommerce implements ProductContract

{

   /** @var App\Store */

   protected $store;

   

   /** @var GuzzleHttp\Client */

   protected $client;

   

   /** @var string */

   protected $uri = 'wp-json/wc/v3/';

 

   public function __construct(Store $store)

   {

       $this->store = $store;

       

       $this->client = new Client([

           'base_uri' => $this->store->base_url . $this->uri,

           'handler' => $this->handler()

       ]);

   }

   

   public function createProduct(Product $product): int

   {

       $product = $this->transformProduct($product);

       

       try {

           $response = $this->client->post('products', [

               'auth' => 'oauth',

               'form_params' => $product

           ]);

           

           $result = json_decode($response->getBody()->getContents());

           

           return (int) $result->id;          

       } catch (RequestException $e) {}

       

       return 0;

   }

}

Magento implementation will be very similar but with different query data:

// app/Services/Store/Magento.php

declare(strict_types = 1);

 

namespace App\Services\Store;

 

class Magento implements ProductContract

{

   /** @var App\Store */

   protected $store;

   

   /** @var GuzzleHttp\Client */

   protected $client;

   

   /** @var string */

   protected $uri = 'rest/V1/';

 

   public function __construct(Store $store)

   {

       $this->store = $store;

       

       $this->client = new Client([

           'base_uri' => $this->store->base_url . $this->uri

       ]);

   }

       

   public function createProduct(Product $product): int

   {

       $token = $this->getToken();        

       $product = $this->transformProduct($product);

       

       try {

           $response = $this->client->post('products', [

               'headers' => [

                   'Authorization' => 'Bearer ' . $token

               ],

               RequestOptions::JSON => [

                   'product' => $product

               ]

           ]);

           

           $result = json_decode($response->getBody()->getContents());

           

           return (int) $result->id;          

       } catch (RequestException $e) {}

       

       return 0;

   }

}

As mentioned earlier, we are utilizing factory pattern for WooCommerce and Magento classes:

// app/Services/Store/Factory.php

 

declare(strict_types = 1);

 

namespace App\Services\Store;

 

class Factory

{

   public static function getStore(Store $store)

   {

       if ($store->type_id == Store::TYPE_WOOCOMMERCE) {

           return new WooCommerce($store);

       } elseif ($store->type_id == Store::TYPE_MAGENTO) {

           return new Magento($store);

       }

       

       return null;

   }

}

We additionally need to include a pivot table that will help us map external product IDs to internal product IDs (we do this because our product IDs in our application can be different from product IDs in managed stores and APIs rely on external product IDs when updating or deleting a product):

// database/migrations/2019_01_01_0000003_create_product_store_table.php

 

class CreateProductStoreTable extends Migration

{

   public function up()

   {

       Schema::create('product_store', function (Blueprint $table) {

           $table->integer('product_id')->unsigned();

           $table->integer('store_id')->unsigned();

           $table->integer('external_product_id')->unsigned()->nullable();

 

           $table->primary(['product_id', 'store_id']);

 

           $table->foreign('product_id')

               ->references('id')

               ->on('products')

               ->onDelete('cascade');

 

           $table->foreign('store_id')

               ->references('id')

               ->on('stores')

               ->onDelete('cascade');

       });

   }

 

   public function down()

   {

       Schema::dropIfExists('product_store');

   }

}

We need to also update the Store and Product models to indicate new relationship:

// app/Store.php

public function products()

{

    return $this

        ->belongsToMany(Product::class)

        ->withPivot(['external_product_id']);

}

 

// app/Product.php

public function stores()

{

    return $this

        ->belongsToMany(Store::class)

        ->withPivot(['external_product_id']);

}

Now we can finalize our created method in the ProductObserver class:

// app/Observers/ProductObserver.php

...

public function created(Product $product)

{

    foreach (Store::all() as $store) {

        $service = Factory::getStore($store);

       

        if ($service !== null) {

            $id = $service->createProduct($product);

           

            if ( ! empty($id)) {

                $store

                    ->products()

                    ->attach($product->id, [

                        'external_product_id' => $id

                    ]);

            }

        }

    }

}

Whenever we create a new product it will automatically be published to all stores defined in the system.

And lastly we implement our new Arisan command that will run periodically and publish products (if needed) in external stores that might have failed during created model event:

namespace App\Console\Commands;

 

class PublishProducts extends Command

{

   protected $signature = 'products:publish';

 

   protected $description = 'Publish any unpublished products';

 

   public function __construct()

   {

       parent::__construct();

   }

 

   public function handle()

   {     

       foreach (Store::all() as $store) {

           foreach (Product::all() as $product) {

               $exists = $store

                   ->products()

                   ->where('products.id', $product->id)

                   ->exists();

               

               if ( ! $exists) {

                   $service = Factory::getStore($store);

                   

                   if ($service !== null) {

                       $id = $service->createProduct($product);

 

                       if ( ! empty($id)) {

                           $store

                               ->products()

                               ->attach($product->id, [

                                   'external_product_id' => $id

                               ]);

                       }

                   }

               }

           }

       }

   }

}

This is a sample conceptual implementation for a centralized application to manage almost any e-commerce platform that supports an API (not limited to PHP). Some code (such as PublishProducts command can certainly be tweaked and improved in terms speed.

 

The store-managing app can help you keep tabs on all of your assets without wasting time on repetitive tasks. With improved analytics, centralized stock management, and synchronized order processing, you can focus on expansion. Click To Tweet

 

 

Polcode is an international full-cycle software house with over 1,300 completed projects. Propelled by passion and ambition, we’ve coded for over 800 businesses across the globe. We’re always on the lookout for solutions that improve the business operations of our clients. Contact us to give your existing e-commerce asset a boost.

Let’s Talk About Your Project!

Have an exciting project in mind? Or maybe would like to improve your current setup?
We’d be happy to discuss it with you. Let’s get in touch!

accept



Our Privacy Policy has been updated in line with the new General Data Protection Regulation(GDPR)