Extending API with Add-ons

Main Goal

Add-ons are designed to extend/change the functionality of the API without modifying the core code. You can easily create your own add-on. Within an add-on, you have the following options:

  • Adding a new API endpoint. An API endpoint is a URL such as /shops/items/1, where a specific code is executed when a request is made to this URL.
  • Extending existing functionality to add new or existing data.

Introduction

You can find general information related to creating, installing, and working with add-ons in this documentation.

Extending Existing API Functionality

To extend an existing API endpoint, you need to call the static method extendApiResponse of the class bff\extend\ApiExtensionsHooks in the start() method of your add-on. The extendApiResponse method takes two arguments: the name of the API endpoint, which can be obtained from the API documentation in the description of each API endpoint, and a callback function that takes all or specific data of the extendable API functionality as input parameters and returns the data we want to embed in the final API response as an associative array.

<?php
class Plugin_Example extends Plugin
{
    protected function start()
    {
        bff\extend\ApiExtensionsHooks::extendApiResponse('bbs.items.view', function($data){
                return ['full_name' => $data['name'] . ' ' . $data['lastname']];
            });
    }
}

In the above code, the callback function will create a new array with the key full_name from the elements of the extendable API data array with the keys name and lastname, and this new array will be added to the end of the original extendable API data array and displayed to the user.

To initialize the API functionality extension, you need to chain the build() method after the extendApiResponse method.

<?php
class Plugin_Example extends Plugin
{
    protected function start()
    {
        bff\extend\ApiExtensionsHooks::extendApiResponse('bbs.items.view', function($data){
                return ['full_name' => $data['name'] . ' ' . $data['lastname']];
            })->build();
    }
}

To pass only specific data to the callback function passed to the extendApiResponse method, you can use the addParamsKeys() method, which takes an array of data keys for the callback function as input. The addParamsKeys() method should be chained after the extendApiResponse method and before the build() method.

<?php
class Plugin_Example extends Plugin
{
    protected function start()
    {
        bff\extend\ApiExtensionsHooks::extendApiResponse('bbs.items.view', function($data){
                return ['full_name' => $data['name'] . ' ' . $data['lastname']];
            })->addParamsKeys(['name', 'lastname'])->build();
    }
}

To add data to a specific location in the final JSON API response, such as a nested object, the addInsertKey() method takes as an argument a string listing the dotted keys of the nested object or array to the end of which data will be added to extend the API response. For example, if user is specified as a key.

<?php
class Plugin_Example extends Plugin
{
    protected function start()
    {
        bff\extend\ApiExtensionsHooks::extendApiResponse('bbs.items.view', function($data){
                return ['full_name' => $data['name'] . ' ' . $data['lastname']];
            })->addParamsKeys(['name', 'lastname'])->addInsertKey('user')->build();
    }
}

then the data with the full_name key will be added to the end of the user object

{
    "result": {
        "message": "success",
        "data": {
            "id": "9",
            "name": "John",
            "user": {
                "id": "1",
                "name": "John",
                "lastname": "Snow",
                "phones": [
                    {
                        "v": "0512370437",
                        "m": "05x xxx xxxx"
                    },
                    {
                        "v": "0888132137",
                        "m": "08x xxx xxxx"
                    }
                ],
                "contacts": {
                    "skype": "login"
                },
                "email": "user@example.com",
                "full_name": "John Snow"
            }
        }
    }
}

If the user.contacts parameter is passed to the addInsertKey() method as a key, the data with the full_name key will be added to the end of the contacts object located in the user object.

{
    "result": {
        "message": "success",
        "data": {
            "id": "9",
            "name": "John",
            "user": {
                "id": "1",
                "name": "John",
                "lastname": "Snow",
                "phones": [
                    {
                        "v": "0512370437",
                        "m": "05x xxx xxxx"
                    },
                    {
                        "v": "0888132137",
                        "m": "08x xxx xxxx"
                    }
                ],
                "contacts": {
                    "skype": "login",
                    "full_name": "John Snow"
                },
                "email": "user@example.com"
            }
        }
    }
}

Adding a New API Endpoint

To implement a new API endpoint, you need to define routes first. The \bff\extend\ApiRouteHooks class is used to implement API routing for extensions. This class contains static methods get(), post(), put(), patch(), delete(), which correspond to the main HTTP request methods used by extensions' APIs. The routing method takes a URL as the first argument, which can be used to call the extension's API endpoint. The second argument can be an array of route parameters.

<?php
class Plugin_Example extends Plugin
{
    protected function start()
    {
        \bff\extend\ApiRouteHooks::get('/plugin/users/status', [
            'name' => 'plugin.items.view',
            'callback' => 'plugins\user_online_do\api\UserOnlineApi@pluginEndpoint',
            'auth' => true
        ]);
    }
}

API routes can have the following parameters:

  • name - the name of the route.
  • auth - this parameter determines whether authentication is required to access this route and accepts a value of true or false.
  • callback - the method that will be called. The class name and method name should be separated by the @ symbol, for example: ClassName@methodName.

If callback is not specified in the route parameters, a callback function should be passed as the third parameter to the routing method of the \bff\extend\ApiRouteHooks class.

<?php
class Plugin_Example extends Plugin
{
    protected function start()
    {
        \bff\extend\ApiRouteHooks::get('/plugin/users/status', [
            'name' => 'plugin.items.view',
            'auth' => true
        ], function ($api){ return ['status' => 'online']; });
    }
}

The callback function always receives an instance of the base API class as its first argument, which contains a number of helper methods.

Additional parameters can be specified in the URL of the route, which should be enclosed in curly braces. These parameters will be passed to the callback function.

<?php
class Plugin_Example extends Plugin
{
    protected function start()
    {
        \bff\extend\ApiRouteHooks::get('/plugin/users/{userId}/status/{statusId}', [
            'name' => 'plugin.items.view',
            'auth' => true
        ], function ($api, $userId, $statusId){ 
            return [
                'status' => 'online',
                'status_id' => $statusId,
                'user_id' => $userId
            ]; 
        });
    }
}

Alternatively, a callback function can be passed as the second parameter to the routing methods of the \bff\extend\ApiRouteHooks class, avoiding the need to pass an array of route parameters.

<?php
class Plugin_Example extends Plugin
{
    protected function start()
    {
        \bff\extend\ApiRouteHooks::get('/plugin/users/{userId}/status/{statusId}',
                            function ($api, $userId, $statusId){ 
                                    return [
                                            'status' => 'online',
                                            'status_id' => $statusId,
                                            'user_id' => $userId
                                        ]; 
                            });
    }
}

Class Api methods

getUser()

The method getUser() returns the ID of the current authenticated user, or 0 if the user is not authenticated.

<?php
class Plugin_Example extends Plugin
{
    protected function start()
    {
        \bff\extend\ApiRouteHooks::get('/plugin/users/status',
                            function ($api){
                                    $userId = $api->getUser();
                                    $data = \Users::model()->userData($userId, ['last_activity']);
                            
                                    return [
                                            'status' => 'online',
                                            'last_activity' => $data['last_activity']
                                        ]; 
                            });
    }
}

paramsAll()

The method paramsAll() returns the parameters passed through the HTTP request, if any. The method accepts an array of parameter names as the first parameter, and default values as the second parameter in case any of the parameters were not passed through the HTTP request.

<?php
class Plugin_Example extends Plugin
{
    protected function start()
    {
        \bff\extend\ApiRouteHooks::get('/plugin/users/status',
                            function ($api){
                                    $params = $api->paramsAll(['user_id', 'status'], ['user_id' => 1, 'status' => 'online']);
                                    $data = \Users::model()->userData( $params['user_id'], ['last_activity']);
                            
                                    return [
                                            'status' => 'online',
                                            'last_activity' => $data['last_activity']
                                        ]; 
                            });
    }
}

Recommendations

Code structure

To avoid having start() method contain too much code, it is recommended to extract the API code of the plugin into a separate class and place it in a separate directory, which can be named api. Then simply call that class in the start() method.

Response data structure

If it is expected that the response of the plugin's API endpoint will contain a large amount of data, or if pagination needs to be used, it is advisable to use extensions that provide the ability to transform response data, such as Fractal. You can learn more about it in the documentation.