Custom API

A custom implementation makes sense when you want to connect a yet unsupported headless commerce system with Forntastic which covers one or more concepts covered by the Frontastic abstraction layers:

  • Product
  • Search (for products)
  • Cart & Wishlist
  • Account
  • Content (CMS)

In this case the most sensible approch is implementing the existing interfaces and communicting with your API inside the implementation. Using this way you can talk basically with any API, be it REST(ful), GraphQL or even SOAP.

For all API abstractions you find the interfaces and used value objects in paas/libraries/common/src/php/*ApiBundle/Domain/*Api.php, so for example paas/libraries/common/src/php/ProductApiBundle/Domain/ProductApi.php for the Product-API.

In this example we will implement a basic Product API which will return a single dummy product but provides you with the entry point for HTTP communication. This basically is a three-step process:

1) Change the used engine

2) Extend the product API factory

3) Write the product API

Change the Engine

Inside the config/project.yml we define the used engine (implementation) for each API. By default in a new project this will be commercetools but we want to implement our own new engine acme now. Since we only want to do this for the Product-API right now we adapt the project configuration like this:

configuration:
  […]
  product:
    engine: acme
  […]

You can add additional configuration values there, like access credentials, which will then be available in your factory. For now leave it this simple.

When refreshing a page which uses a product stream you now will get an error that no Product API has been configugured, because this Product API implementation does not exist yet.

Extend The Product API Factory

You can use the default Symfony mechanism of decorators in the Depenedency Injection Container to overwrite the default Product API factory with your own implementation and thus registering your own API. For this add the following lines to a service.xml in one of your project bundles (if there is no bundle yet you can create one using bin/console frontastic:create:bundle ProductApi, for example):

<service id="Acme\ProductApiBundle\Domain\ProductApiFactory"
    decorates="Frontastic\Common\ProductApiBundle\Domain\DefaultProductApiFactory">
    <argument type="service" id="Acme\ProductApiBundle\Domain\ProductApiFactory.inner"/>
</service>

And now we also need to create this factory class, in this case in the file ProductApiBundle/Domain/ProductApiFactory.php inside the src/php folder in your project.

<?php

namespace Acme\ProductApiBundle\Domain;

use Frontastic\Common\ProductApiBundle\Domain\ProductApiFactory as BaseProductApiFactory;
use Frontastic\Common\ProductApiBundle\Domain\ProductApi as BaseProductApi;
use Frontastic\Common\ReplicatorBundle\Domain\Project;

class ProductApiFactory implements BaseProductApiFactory
{
    private $aggregate;

    public function __construct(BaseProductApiFactory $aggregate)
    {
        $this->aggregate = $aggregate;
    }

    public function factor(Project $project): BaseProductApi
    {
        $productConfig = $project->getConfigurationSection('product');

        switch ($productConfig->engine) {
            case 'acme':
                return new ProductApi();

            default:
                return $this->aggregate->factor($project);
        }
    }
}

The ProductApi class obviously does not exist yet, this we must implement this in the next & last step.

Implement the Product API

A Product API must implement the corresponding interface and you can do in there whatever you want. To enable parallel fetching of streams you should always return promises for all data (where documented). This a minimal implementation can look like:

<?php

namespace Acme\ProductApiBundle\Domain;

use Frontastic\Common\ProductApiBundle\Domain\ProductApi as BaseProductApi;
use Frontastic\Common\ProductApiBundle\Domain\ProductApi\Query\CategoryQuery;
use Frontastic\Common\ProductApiBundle\Domain\ProductApi\Query\ProductQuery;
use Frontastic\Common\ProductApiBundle\Domain\ProductApi\Query\ProductTypeQuery;
use Frontastic\Common\ProductApiBundle\Domain\ProductApi\Result;
use Frontastic\Common\ProductApiBundle\Domain\Product;
use Frontastic\Common\ProductApiBundle\Domain\Variant;
use Frontastic\Common\ProductApiBundle\Domain\Category;
use Frontastic\Common\ProductApiBundle\Domain\ProductType;

use GuzzleHttp\Promise\PromiseInterface;
use GuzzleHttp\Promise\FulfilledPromise;

class ProductApi implements BaseProductApi
{
    /**
     * @param CategoryQuery $query
     * @return Category[]
     */
    public function getCategories(CategoryQuery $query): array
    {
        return [];
    }

    /**
     * @param ProductTypeQuery $query
     * @return ProductType[]
     */
    public function getProductTypes(ProductTypeQuery $query): array
    {
        return [];
    }

    /**
     * @param ProductQuery $query
     * @param string $mode One of the QUERY_* connstants. Execute the query synchronously or asynchronously?
     * @return Product|PromiseInterface|null A product or null when the mode is sync and a promise if the mode is async.
     */
    public function getProduct(ProductQuery $query, string $mode = self::QUERY_SYNC): ?object
    {
        return new FulfilledPromise(
            new Product([
                'name' => 'My Dmmy Test Product',
                'variants' => [
                    new Variant(),
                ],
            ])
        );
    }

    /**
     * @param ProductQuery $query
     * @param string $mode One of the QUERY_* connstants. Execute the query synchronously or asynchronously?
     * @return Result|PromiseInterface A result when the mode is sync and a promise if the mode is async.
     */
    public function query(ProductQuery $query, string $mode = self::QUERY_SYNC): object
    {
        return new FulfilledPromise(
            new Result([
                'items' => [
                    new Product([
                        'name' => 'My Dmmy Test Product',
                        'variants' => [
                            new Variant(),
                        ],
                    ]),
                ],
            ])
        );
    }

    /**
     * Get *dangerous* inner client
     *
     * This method exists to enable you to use features which are not yet part
     * of the abstraction layer.
     *
     * Be aware that any usage of this method might seriously hurt backwards
     * compatibility and the future abstractions might differ a lot from the
     * vendor provided abstraction.
     *
     * Use this with care for features necessary in your customer and talk with
     * Frontastic about provising an abstraction.
     *
     * @return mixed
     */
    public function getDangerousInnerClient()
    {
        return null;
    }
}