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.
On-demand webinar: Moving Forward From Legacy Systems
We’ll walk you through how to think about an upgrade, refactor, or migration project to your codebase. By the end of this webinar, you’ll have a step-by-step plan to move away from the legacy system.
Latest blog posts
Ready to talk about your project?
Tell us more
Fill out a quick form describing your needs. You can always add details later on and we’ll reply within a day!
Strategic Planning
We go through recommended tools, technologies and frameworks that best fit the challenges you face.
Workshop Kickoff
Once we arrange the formalities, you can meet your Polcode team members and we’ll begin developing your next project.