Custom APIs

A custom implementation makes sense when you want to connect a yet unsupported headless commerce system with Frontastic, 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 approach is implementing the existing interfaces and communicating 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, for example paas/libraries/common/src/php/ProductApiBundle/Domain/ProductApi.php for the Product-API.

In this example, we'll 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 Engine that's being used
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, like access credentials, which will then be available in your factory.

When refreshing a page which uses a product stream you'll now get an error that no Product API has been configured, this is because the Product API implementation doesn't exist yet.

2
Extend the Product API Factory
You can use the default Symfony mechanism of decorators in the Dependency 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 isn't a 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>

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);
        }
    }
}
3
Implement the ProductAPI class
A Product API must implement the corresponding interface and you can do whatever you want within it. To enable parallel fetching of streams you should always return promises for all data (where documented). The below is an example of what a minimal implementation could 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_* constants. 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 Dummy Test Product',
                'variants' => [
                    new Variant(),
                ],
            ])
        );
    }

    /**
     * @param ProductQuery $query
     * @param string $mode One of the QUERY_* constants. 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 aren't part
     * of the abstraction layer yet.
     *
     * Be aware that any usage of this method might seriously impact 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 providing an abstraction.
     *
     * @return mixed
     */
    public function getDangerousInnerClient()
    {
        return null;
    }
}

‹ Back to Article List

Next Article ›

Custom Fields

Still need help? Contact Us Contact Us