Magento 2 REST API: Part 5, Batch and Extending the API

The way we handle our inventory is to do a daily export of pricing and quantity from our inventory management system. This “export” has a legacy function of generating CSV files for uploading to various sales channels. Enter a plan to modernize, we kept the process and naming the same for our people, but created API calls that send out a batch of products. Magento by design takes one item at a time. Good or bad, this is just how it is. I found things a little slow, and wanted to be able to send over a batch of products, so I looked into extending the API.

Extending the API is actually pretty simple with Magento 2. You will need to make a custom module that extends the REST API and then start writing code. Here’s a list of the files I have in mine:

app/code/Danjoseph/Rest/etc/frontend/routes.xml
app/code/Danjoseph/Rest/etc/di.xml
app/code/Danjoseph/Rest/etc/module.xml
app/code/Danjoseph/Rest/etc/webapi.xml
app/code/Danjoseph/Rest/Api/BatchproductsInterface.php
app/code/Danjoseph/Rest/Model/Batchproducts.php
app/code/Danjoseph/Rest/Controller/Catalog/Products.php
app/code/Danjoseph/Rest/registration.php

Since this isn’t a lesson on creating a basic module, I am going to go through this as if you have a light understanding of a basic Hello World module. You can pick up on how to build a module with this tutorial just fine. However, If you want to read about one, click here. You will see some links to other blogs in here explaining basic module things, and I am going to throw in a controller just for the fun of it. It will just have a simple echo.

The JSON:

[{
    "sku": "XYZ_123456",
    "name": "alksjdflakjsdflkjsaldf",
    "description": "arooo!!!!",
    "urlkey": "xyz-123456"
}, {
    "sku": "XYZ_789012",
    "name": "34875938745987345",
    "description": "yah!!!!",
    "urlkey": "xyz-789012"
}]

Let’s get two things out of the way real quick. The registration file and the basic controller:

registration.php

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
    \Magento\Framework\Component\ComponentRegistrar::MODULE,
    'Danjoseph_Rest',
    __DIR__
);

Controller/Catalog/Products.php

<?php

namespace Danjoseph\Rest\Controller\Catalog;

class Products extends \Magento\Framework\App\Action\Action
{
    public function __construct(
        \Magento\Framework\App\Action\Context $context)
    {
        return parent::__construct($context);
    }

    public function execute()
    {
        echo "Hello, World!";
        exit();
    }
}

Those two will give you a nice hello world message for “http://storeurl.com/djrest/catalog/product”.

Now let’s go through the breakdown of files, starting with the XML files.

etc/frontend/routes.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../../lib/internal/Magento/Framework/App/etc/routes.xsd">
    <router id="standard">
        <route id="djrest" frontName="djrest">
            <module name="Danjoseph_Rest" />
        </route>
    </router>
</config>

This sets up our frontend. Essentially what we’re doing here is creating “http://storeurl.com/djrest”.

etc/di.xml

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/ObjectManager/etc/config.xsd">
    <preference for="Danjoseph\Rest\Api\BatchproductsInterface" type="Danjoseph\Rest\Model\Batchproducts" />
</config>

This is our di (Dependency Injection). We need an interface and model for our batch products API functionality.

etc/module.xml

<?xml version="1.0"?>

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../lib/internal/Magento/Framework/Module/etc/module.xsd">
    <module name="Danjoseph_Rest" setup_version="1.0.0" schema_version="2.0.0"></module>
</config>

Our version and our module activation XML.

etc/webapi.xml

<?xml version="1.0"?>
<routes xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../../../../../app/code/Magento/Webapi/etc/webapi.xsd">
    <route url="/V1/catalog/batchproducts/add" method="POST">
        <service class="Danjoseph\Rest\Api\BatchproductsInterface" method="add"/>
        <resources>
            <resource ref="Magento_Catalog::products"/>
        </resources>
    </route>
    <route url="/V1/catalog/batchproducts/update" method="POST">
        <service class="Danjoseph\Rest\Api\BatchproductsInterface" method="update"/>
        <resources>
            <resource ref="Magento_Catalog::products"/>
        </resources>
    </route>
</routes>

This is where you’re going to set up your routes and access to your new API. I have two of them for our example. They’re basically setup the same. This is pretty simple, let’s break it down:

<route url="/V1/catalog/batchproducts/add" method="POST">

To keep in the spirit of versioning and the Magento 2 REST API architecture, I made my URL like they made theirs. For this particular call, I am asking for a POST.

<service class="Danjoseph\Rest\Api\BatchproductsInterface" method="add"/>

Your service class can be anything you want. Since we have all of our REST API code under one module, I simply set the class to my batch products interface (coming up). The method (or php function) can be anything you want. I like human words and readability, so I went with “add”. After all, we are “add”ing a batch of products.

<resources>
    <resource ref="Magento_Catalog::products"/>
</resources>

Finally, we have our resources. This is where you can tie it into the access setup for our API user. Magento’s web site has a great breakdown on the webapi.xml. You can have anonymous APIs, ones specific to access granted to specific parts of the API (Like I did), or add in multiple areas.

Api/BatchproductsInterface.php

<?php

namespace Danjoseph\Rest\Api;

interface BatchproductsInterface
{
    /**
     * Return a list of IDs with SKU association
     *
     * @api
     * @return int The sum of the SKUs.
     */
    public function add();

    /**
     * Return a true/false after update with list of SKU associations
     *
     * @api
     * @return int The sum of the SKUs.
     */
    public function update();
}

If you’re not familiar with interfaces, TechFlirt.com has a good write up on them. Remember the line of your webapi.xml? This is what it refers too. This is your average interface with some comments.

Rest/Model/Batchproducts.php

<?php

namespace Wundercarparts\Wcprest\Model;

use Symfony\Component\Config\Definition\Exception\Exception;
use Danjoseph\Rest\Api\BatchproductsInterface;

class Batchproducts implements BatchproductsInterface
{
    public function __construct( \Magento\Framework\App\Helper\Context $context,
                                 \Magento\Catalog\Model\Product $_product        )
    {
        $this->_product = $_product;
        $this->_eventManager = $context->getEventManager();;
    }

    /**
     * @api
     * @return int
     */
    public function add()
    {
        $products = json_decode( file_get_contents("php://input"), true );
        $ids      = array();

        foreach ( $products as $product )
        {
            $ids[] = $this->addProduct( $product );
        }

        return count( $ids );
    }

    private function addProduct( array $product )
    {
        set_time_limit( 86400 );

        $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
        $newProduct    = $objectManager->create('\Magento\Catalog\Model\Product');
        $newProduct->setSku( $product['sku'] );
        $newProduct->setName( $product['name'] );
        $newProduct->setDescription( $product['description'] );
        $newProduct->setUrlKey( $product['urlkey'] );
        $newProduct->setStatus( 1 );
        $newProduct->setVisibility( 4 );
        $newProduct->setTypeId( "simple" );

        $newProduct->save();
        $newProductId = $newProduct->getId();

        return $newProductId;
    }

We’re finally to the point where we’re just writing Magento and PHP code. I am under the opinion that methods should only have one thought, and be short. I’ve broken this up into two functions as a result. “add” is the one you references in your XML files. This is extended from the interface we just created. Let’s break this down a little bit:

$products = json_decode( file_get_contents("php://input"), true );

This is how you capture your POST data. For the sake of this tutorial, I kept things simple, but there are various resources all over the internet on how to properly design a REST API, and it would be a good idea to validate that this is a POST. You will also want to toss back various http codes (200, 404, 500, etc) for different reactions. Doing a return $blah; will trigger a 200 and send that return information to the REST client.

You’ll see all the magic being done in the addProducts function. I’m creating a new product, setting a few basic attributes, and then returning the product ID. My add function will then tally them up, and return a count of how many were created. You should add error handling to your creation, and tailor your requests that way you want them. You can even write a log file (Inchoo has a good Logger post).

That’s the basics. If you’re new to Magento 2, like I was, you’ll find this a fun way to learn about various parts of the new framework. You can easily add to the REST API in structure Magento 2, or create your own offshoots with the routing. Write something of your own. See how far you can expand it. The boundaries for the REST API are now gone.

-Dan

Written by Dan
Welcome to my blog! Here you'll find my collection of bible study lessons, book reviews, and other posts I feel inspired to write. I am a Christian currently serving at First Baptist Church of Northville in Northville, MI.