lushdigital/microservice-aggregator-transport

This package is not installable via Composer 1.x, please make sure you upgrade to Composer 2+. Read more about our Composer 1.x deprecation policy.

A set of convenience classes and interfaces for simplifying aggregation of data from multiple microservices.

1.1.1 2017-12-05 11:14 UTC

This package is not auto-updated.

Last update: 2021-07-18 16:09:36 UTC


README

A set of convenience classes and interfaces for simplifying aggregation of data from multiple microservices.

The purpose of the package is to provide a reliable, testable and easy to use means of communicating with microservices within a service oriented architecture.

Package Contents

  • Service interface
  • Service abstract class
  • Cloud service interface
  • Cloud service abstract class
  • Request class

Installation

Install the package as normal:

$ composer require lushdigital/microservice-aggregator-transport

Copy the src/config/transport.php file into your config folder in the root of your app.

Finally add the following line to your bootstrap/app.php file:

$app->configure('transport');

Creating a Service

The first thing you need to do to utilise this package is to create a class to interact with your service.

This class will extend one of the base classes this package provides; you add your own methods for each endpoint of the service you want to access.

Local Service

A 'local' service is used when you can communicate with a service via some kind of local DNS. In that you do not need to call out over the internet to access the service. For example you might be using Kubernetes DNS.

Before you can create a cloud service you need to ensure the following config options are set (explicitly or via environment variables):

  • transport.branch - The CI branch. For example master.
  • transport.environment - The CI environment. For example dev or staging.

Then for each local service, you must define:

  • transport.services.local.SERVICE_NAME.uri - The URI of the local service.

You can also optionally specify a version of a service:

  • transport.services.local.SERVICE_NAME.version - The version of the local service.

To create a local service you need to extend the \LushDigital\MicroserviceAggregatorTransport\Service class:

<?php
/**
 * @file
 * Contains \App\Services\MyAwesomeService.
 */

namespace App\Services;

use LushDigital\MicroserviceAggregatorTransport\Service as BaseService;
use LushDigital\MicroserviceAggregatorTransport\Request;
use App\Models\Thing;

/**
 * Transport layer for my awesome service.
 *
 * @package App\Services
 */
class MyAwesomeService extends BaseService
{
    /**
     * Save a thing.
     *
     * @param Thing $thing
     *     The thing to save.
     *
     * @return array 
     */
    public function saveAThing(Thing $thing)
    {
        // Create the request.
        $request = new Request('things', 'POST', $thing->toArray());

        // Do the request.
        $this->dial($request);
        $response = $this->call();

        return !empty($response->data->things) ? $response->data->things : [];
    }
}

As you can see in this service we have created a method which calls a POST endpoint to save a thing.

Cloud Service

A cloud service is used when you need to communicate with a service over the internet. The assumption is that the service is accessed via some kind of API gateway and can't be accessed directly.

Before you can create a cloud service you need to ensure the following config options are set (explicitly or via environment variables):

  • transport.branch - The CI branch. For example master.
  • transport.domain - The top level domain of the service environment.
  • transport.gateway_uri - The URI of the API gateway.
  • transport.environment - The CI environment. For example dev or staging.

Then for each cloud service, you must define:

  • transport.services.cloud.SERVICE_NAME.uri - The URI of the cloud service.
  • transport.auth.SERVICE_NAME.email - The email address of a service account used to access the cloud service.
  • transport.auth.SERVICE_NAME.password - The password of a service account used to access the cloud service.

You can also optionally specify a version of a service:

  • transport.services.cloud.SERVICE_NAME.version - The version of the cloud service.

Then you can define your service:

<?php
/**
 * @file
 * Contains \App\Services\MyAwesomeService.
 */

namespace App\Services;

use LushDigital\MicroserviceAggregatorTransport\CloudService;
use LushDigital\MicroserviceAggregatorTransport\Request;
use App\Models\Thing;

/**
 * Transport layer for my awesome cloud service.
 *
 * @package App\Services
 */
class MyAwesomeCloudService extends CloudService
{
    /**
     * Save a thing.
     *
     * @param Thing $thing
     *     The thing to save.
     *
     * @return array 
     */
    public function saveAThing(Thing $thing)
    {
        // Create the request.
        $request = new Request('things', 'POST', $thing->toArray());

        // Do the request.
        $this->dial($request);
        $response = $this->call();

        return !empty($response->data->things) ? $response->data->things : [];
    }
}

As you can the service looks very similar to a local one. The only major difference is the base class. The base class does all the heavy lifting of authentication and API gateway routing so you don't have to!

Using a Service

Once you have created your service it can be used just like any other PHP class. Think of them like you would a repository object in a database environment.

Example usage in a controller:

<?php
/**
 * @file
 * Contains \App\Http\Controllers\MyAwesomeController.
 */

namespace App\Http\Controllers;

use App\Models\Thing;
use App\Services\MyAwesomeService;
use App\Services\MyAwesomeCloudService;
use GuzzleHttp\Exception\BadResponseException;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Lumen\Routing\Controller as BaseController;
use LushDigital\MicroserviceAggregatorTransport\ServiceInterface;

class MyAwesomeController extends BaseController
{
    /**
     * Transport layer for my awesome service.
     *
     * @var ServiceInterface
     */
    protected $myAwesomeService;
    
    /**
     * Transport layer for my awesome cloud service.
     *
     * @var ServiceInterface
     */
    protected $myAwesomeCloudService;
    
    /**
     * MyAwesomeController constructor.
     */
    public function __construct()
    {
        $this->myAwesomeService = new MyAwesomeService();
        $this->myAwesomeCloudService = new MyAwesomeCloudService();
    }
    
    /**
     * Create a new thing.
     *
     * @param Request $request
     * @return Response
     */
    public function storeThing(Request $request)
    {
        // Validate the request.
        $this->validate($request, ['name' => 'required|string']);
        
        try {
            // Prepare a thing.
            $thing = new Thing;
            $thing->fill($request->input());
               
            // Save a thing.
            $newThing = $this->myAwesomeService->saveAThing($thing);
            
            return response()->json($newThing, 200);
        } catch (BadResponseException $e) {
            return response()->json(null, 500);
        }
    }
    
    /**
     * Create a new thing.
     *
     * @param Request $request
     * @return Response
     */
    public function storeCloudThing(Request $request)
    {
        // Validate the request.
        $this->validate($request, ['name' => 'required|string']);
        
        try {
            // Prepare a thing.
            $thing = new Thing;
            $thing->fill($request->input());
               
            // Save a thing.
            $newThing = $this->myAwesomeCloudService->saveAThing($thing);
            
            return response()->json($newThing, 200);
        } catch (BadResponseException $e) {
            return response()->json(null, 500);
        }
    }
}

Asynchronous Requests

In some situations you made need to make multiple requests in quick succession. This can often be improved by running the requests concurrently. The package utilises Guzzle promises to do just that. First you need to define a service with an asynchronous call:

<?php
/**
 * @file
 * Contains \App\Services\MyAwesomeService.
 */

namespace App\Services;

use LushDigital\MicroserviceAggregatorTransport\Service as BaseService;
use LushDigital\MicroserviceAggregatorTransport\Request;
use GuzzleHttp\Promise\PromiseInterface;
use App\Models\Thing;

/**
 * Transport layer for my awesome service.
 *
 * @package App\Services
 */
class MyAwesomeService extends BaseService
{
    /**
     * Save a thing.
     *
     * @param Thing $thing
     *     The thing to save.
     * @param callable|null $onFulfilled
     *     Function to run on a successful call.
     * @param callable|null $onRejected
     *     Function to run on a rejected call. 
     *
     * @return PromiseInterface 
     */
    public function saveAsyncThing(Thing $thing, callable $onFulfilled = null, callable $onRejected = null)
    {
        // Create the request.
        $request = new Request('things', 'POST', $thing->toArray());

        // Do the request.
        $this->dial($request);
        
        return $this->callAsync($onFulfilled, $onRejected);
    }
}

You can then use this in your controller:

<?php
/**
 * @file
 * Contains \App\Http\Controllers\MyAwesomeController.
 */

namespace App\Http\Controllers;

use App\Models\Thing;
use App\Services\MyAwesomeService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Laravel\Lumen\Routing\Controller as BaseController;
use LushDigital\MicroserviceAggregatorTransport\ServiceInterface;
use GuzzleHttp\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;

class MyAwesomeController extends BaseController
{
    /**
     * Transport layer for my awesome service.
     *
     * @var ServiceInterface
     */
    protected $myAwesomeService;
    
    /**
     * Transport layer for my awesome cloud service.
     *
     * @var ServiceInterface
     */
    protected $myAwesomeCloudService;
    
    /**
     * MyAwesomeController constructor.
     */
    public function __construct()
    {
        $this->myAwesomeService = new MyAwesomeService();
    }
    
    /**
     * Create a new thing.
     *
     * @param Request $request
     * @return Response
     */
    public function storeThing(Request $request)
    {
        // Validate the request.
        $this->validate($request, ['things.*.name' => 'required|string']);
        
        $promises = [];
        $things = [];
        foreach ($request->input('things') as $thingData) {
            // Prepare a thing.
            $thing = new Thing;
            $thing->fill($thingData);
            
            // Save a thing.
            $promises[] = $this->myAwesomeService->saveAsyncThing(
                $thing, 
                function (ResponseInterface $res) use (&$things) {
                    // Get the JSON response or exit if none.
                    $serviceResponse = json_decode($res->getBody());
                    if (empty($serviceResponse->data)) {
                        return null;
                    }
                
                    $things[] = $serviceResponse->data->things[0];
                },
                function (RequestException $e) {
                    Log::error(sprintf('Could not get thing. Reason: %s', $e->getMessage()));
                    return null;
                }
            );
        }
            
        return response()->json($things, 200);
    }
}