PHP
Article
By Reza Lavaryan

RESTful Remote Object Proxies with ProxyManager

By Reza Lavaryan

This article was peer reviewed by Deji Akala and Marco Pivetta. Thanks to all of SitePoint’s peer reviewers for making SitePoint content the best it can be!

The proxy pattern is another cool design pattern in software development. A proxy is a class working as an interface to another class or web service. For the sake of simplicity, we’ll refer to proxied classes as subjects throughout the rest of the article. A proxy usually implements the same interface as the subject, so it looks like we’re calling the methods directly on the subject.

Although this article is not about the proxy pattern concept, we’ll discuss some basics, just to get started.

There are a variety of proxy types used for different purposes:

  1. Virtual Proxy – for lazily loading the resource-hungry objects, preventing them from occupying memory until they are needed.

  2. Protection Proxy – for limiting access to properties or methods of an object through a set of rules.

  3. Smart Reference – for adding additional behaviors when a method is called on the subject – suitable for aspect-oriented programming.

  4. Remote Objects – for accessing remote objects, hiding the fact that they are actually in a separate address space. This article will be mainly covering remote object proxies.

Let’s get started with a basic example of a virtual proxy:

class MyObjectProxy
{
    protected $subject;

    public function someMethod()
    {
        $this->init();

        return $this->subject->someMethod();
    }

    private function init()
    {
        if (null === $this->subject) {
            $this->subject = new Subject();
        }
    }
}

In the above code, we’re keeping a reference to the subject; each time a method is called against the proxy, init() is invoked, checking whether the subject has been instantiated yet, and if not, it will instantiate it.

Finally, the respective method on the subject is called:

// ...
return $this->subject->someMethod();
// ...

This is a very basic example of a value holder virtual proxy, which keeps a reference to the subject all the times. Although the previous example works just fine, this is not how we create proxies in this world. Instead, we’ll do what any other good developer would do: use a well-known, well-tested third-party library.

ProxyManager is a PHP library for creating various kinds of proxies through a set of factory classes.

Marco Pivetta, the author of ProxyManager, has a comprehensive presentation, which is an easy-to-understand introduction to the Proxy pattern. I also recommend taking a quick look at the official documentation of ProxyManager before we start.

Cutting to the Chase

This post is dedicated to one of the less-discussed proxy types known as remote object proxies. We will learn what they are and how they work. Then, we’ll move on to creating a remote object proxy, which is capable of interacting with a RESTful API.

Simply put, remote object proxies are used to interact with remote subjects (using HTTP as the transport mechanism) and disguise them as local objects!

Whenever a method is called against the proxy object, it tries to map the called method name to its remote counterpart. Then, it encodes and forwards the request to the remote method. All this happens in a matter of milliseconds.

We create our remote object proxies by using the RemoteObjectFactory class of ProxyManager. This factory requires an adapter to create a remote object proxy, capable of interacting with a certain remote subject. On the other hand, the adapter is responsible for transforming a request in a way that the remote subject can handle.

The adapters vary based on the implemented protocol by the remote subject; whether the subject is XML-RPC based, SOAP based or even a RESTful JSON API, the appropriate adapter should be used.

The code which follows will be using ProxyManager’s RemoteObjectFactory.

Currently, ProxyManager provides three adapters out of the box: XmlRpc, JsonRpc, and Soap. These adapters use an implementation of the Zend\Server\Client interface as a transporting mechanism.

To see examples of how it’s done, the official documentation is the best place.

In the next section, we will create what is lacking at the moment: a custom adapter suitable for RESTful APIs!

Preparation

First, we’ll create a dummy JSON API as our remote subject – using Silex as our framework. Then, we’ll install ProxyManager to create a proxy for it.

Since ProxyManager doesn’t provide an adapter for RESTful APIs, we need to create our own. Our custom adapter will use Guzzle as its HTTP client.

Let’s start off by creating a directory to keep our files in:

mkdir rest-proxy && cd rest-proxy

Now, we install Silex, ProxyManager, and Guzzle (for now):

composer require silex/silex ocramius/proxy-manager guzzlehttp/guzzle

Directory Structure

We will implement the test API and the proxy-related code in the same project, pretending that one is local and the other one is remote. For the sake of readability, we put these two components under different namespaces.

That said, we need to change the autoload key of our composer.json file:

Filename: composer.json

{
    ...

    "autoload": {
        "psr-4": {
            "": "src/"
        }
    }
}

Then we run composer dump-autoload.

As a result, we can have two different namespaces, mapped to two different directories within the src/ directory. Let’s name them RemoteProxy and API respectively. In addition, we create another directory named web in the project’s root directory to keep our index.php file. The directory structure should look like this now:

remote-proxy
├── src
│   ├── Api
│   └── RemoteProxy
└── web

Creating the API

The API is going to have three endpoints for returning a list of books, details of a book, and authors of a book.

To do this, we create a Silex controller provider under src/Api/Controller as BookControllerProvider.php:

To see how Silex controller providers work, you might want to take a look at this page.

Filename: src/Api/Controller/ApiControllerProvider.php

<?php

namespace Api\Controller;

use Silex\Application;
use Silex\Api\ControllerProviderInterface;
use Symfony\Component\HttpFoundation\JsonResponse as Json;

class ApiControllerProvider implements ControllerProviderInterface
{

    public function connect(Application $app)
    {
        $controllers = $app['controllers_factory'];

        /**
         * Return a list of the books
         */
        $controllers->get('books', function (Application $app) {

            return new Json([
                'books' => 'List of books...'
            ]);
        });

        /**
         * Return book details by Id
         */
        $controllers->get('books/{id}', function (Application $app, $id) {

            return new Json([
                'details' => 'Details of book with id ' . $id,
            ]);
        });

        /**
         * Return the author(s) of the book
         */
        $controllers->get('books/{id}/authors', function (Application $app, $id) {

            return new Json([
                'authors' => 'Authors of book with id ' . $id,
            ]);
        });

        return $controllers;
    }
}

We have three routes with controllers, each of which returns an instance of JsonResponse with a dummy message as content.

Creating the Application

Next, we create our src/app.php file, where we create the application and mount our controller provider:

Filename: src/app.php

<?php

use Silex\Application;
use Api\Controller\ApiControllerProvider;

$app = new Application();
$app->mount('/api/v1', new ApiControllerProvider());

return $app;

Front Controller

Finally, we create the front controller, which will be the entry point to our API. We save this file as web/index.php with the following content:

Filename: web/index.php

<?php
require_once __DIR__.'/../vendor/autoload.php'; 
$app = require __DIR__ . '/../src/app.php';
$app->run(); 

In the above code, we include the composer autoloader and our app.php file (which returns a Silex application object). Finally we run the application by calling the run() method.

To test the code, refer to your environment’s server setup (automatic if using something like Homestead Improved) or for brevity, use PHP’s built-in web server:

php -S localhost:9000 -t web

As a result of the latter option, the API will be accessible under http://localhost:9000/api/v1 from the browser.

Creating the Proxy

Now that we have everything set up on the “remote” side, it is time to create some proxies!

The Proxy Adapter

The adapters in ProxyManager extend the abstract class BaseAdapter, which implements AdapterInterface.

All adapters accept an HTTP client object (via their constructor) for interacting with the remote subject, along with an array, which contains method-to-endpoint mapping information. By using this array, the adapter will know to which remote endpoint it should forward the request.

In our case, the array looks like this:

<?php
// ...

$array = [
    'getBooks'   => ['path' => 'books',             'method' => 'get'],
    'getBook'    => ['path' => 'books/:id',         'method' => 'get'],
    'getAuthors' => ['path' => 'books/:id/authors', 'method' => 'get'],
];

// ...

Each key is a method name (on the proxy object) and the value (which is an array) has the remote path with the method to access it. We’ll see how this array is used later.

For creating our adapter, unlike other adapters, we won’t extend the BaseAdapter abstract class. Instead, we’ll create it from scratch. Why do this? Because the abstract class forces us to use an implementation of Zend\Server\Client as our HTTP client, whereas we want to use Guzzle.

The adapters (implementing AdapterInterface) must implement call(), which is invoked by the proxy whenever a method is called. This method has three arguments: the proxied class itself, the called method’s name, and the called method’s arguments.

Here’s the code of our RESTful adapter:

Filename: src/RemoteProxy/Adapter/RestAdapter.php

<?php

namespace RemoteProxy\Adapter;

use GuzzleHttp\ClientInterface;
use ProxyManager\Factory\RemoteObject\AdapterInterface;

class RestAdapter implements AdapterInterface
{

    /**
     * Adapter client
     *
     * @var GuzzleHttp\ClientInterface
     */
    protected $client;

    /**
     * Mapping information
     *
     * @var array
     */
    protected $map;

    /**
     * Constructor
     *
     * @param Client $client
     * @param array  $map    Map of service names to their aliases
     */
    public function __construct(ClientInterface $client, $map = [])
    {
        $this->client  = $client;
        $this->map     = $map;
    }

    /**
     * {@inheritDoc}
     */
    public function call($wrappedClass, $method, array $parameters = [])
    {
        if (!isset($this->map[$method])) {
            throw new \RuntimeException('No endpoint has been mapped to this method.');
        }

        $endpoint =  $this->map[$method];
        $path     = $this->compilePath($endpoint['path'], $parameters);

        $response = $this->client->request($endpoint['method'], $path);

        return (string) $response->getBody();
    }

    /**
     * Compile URL with its values
     *
     * @param string $path
     * @param array $parameters
     *
     * @return string
     */
    protected function compilePath($path, $parameters)
    {
        return preg_replace_callback('|:\w+|', function ($matches) use (&$parameters) {
            return array_shift($parameters);
        }, $path);
    }
}

In the preceding the code, first, we set the properties $map and $client in the class constructor. Inside the call() method, we check if the called method name exists in the $map property; if it does, we call compilePath(), putting the called method’s arguments into their respective placeholders in the URL.

For example, if we call getBook(12), the URL could be something like http://localhost:9000/api/v1/book/12.

Finally, we send a request according to the specified HTTP verb – using the request() method of our Guzzle client:

// ...
$response = $this->client->request($endpoint['method'], $path);
// ...

Creating the Proxy Object

Now, any time we need to create a remote object proxy, we create an instance of ProxyManager\Factory\RemoteObjectFactory, passing it an instance of our custom adapter and the mapping array.

This procedure should be repeated anywhere we need to use the proxy.

Usage example:

<?php

// ...

use ProxyManager\Factory\RemoteObjectFactory;
use RemoteProxy\Adapter\RestAdapter;

$base_uri = 'http://localhost';
// Creating the factory
$factory = new RemoteObjectFactory(
            // Passing our custom adapter in
            new RestAdapter(
                // Passing our HTTP client to the adapter
                new \GuzzleHttp\Client([
                    'base_uri' => $base_uri,
                ]),
                // Along with the arraym which contain the method-to-endpoint mapping
                $array = [
                  'getBooks'   => ['path' => 'books',             'method' => 'get'],
                  'getBook'    => ['path' => 'books/:id',         'method' => 'get'],
                  'getAuthors' => ['path' => 'books/:id/authors', 'method' => 'get'],
                ];
            )
        );

$factory->createProxy('interfaceName');

// ...

Please note that the base URI that we pass to Guzzle at the instantiation time is the base URI of our API.

Now, we create our proxy object by calling createProxy() on the factory. This method requires an interface the methods of which are implemented by the remote subject. That said, we need to have the interface as well. Let’s put this file under the RemoteProxy namespace:

File: src/RemoteProxy/LibraryInterface.php

<?php

namespace RemoteProxy;

interface LibraryInterface
{    
    /**
     * Return the books
     */
    public function getBooks();

    /**
     * Return book's details
     *
     * @param  int $id
     * @return  mixed
     */
    public function getBook($id);

    /**
     * Return author of a book
     *
     * @param  int $id
     * @return mixed
     */
    public function getAuthors($id);
}

Now that we have the interface, we can create the proxy:

<?php

// ...

$library = $factory->createProxy(RemoteProxy\LibraryInterface::class);

// calling the methods:

var_dump($library->getBooks());
var_dump($library->getAuthors(1));

// ...

Keeping the Mapping Information with the Interface

Although we can prepare the mapping array at the time of instantiating the adapter, there’s a cleaner way to do it. We define the endpoints with each method in the interface, by using a custom annotation.

Something like:

<?php

interface LibraryInterface
{
    /**
     * Return all books
     * @Endpoint(path="/books", method="get")
     */
    public function getBooks();

    // ...
}

A custom annotation named Endpoint has been used, specifying a path and method to access it.

Creating a custom annotation is a very simple task. Each annotation is a class of the same name, with annotation parameters as its public properties. We parse this information with an annotation parser into an array, which can be sent to our adapter.

To create and parse the annotation we use the Doctrine Annotations library.

composer require doctrine/annotations

Each annotation class should be annotated with @Annotation, so Doctrine’s annotation parser will know it’s a valid annotation class.

Let’s put the annotation class inside src/RemoteProxy/Annotation, so we can have it under the Annotation namespace:

File: src/RemoteProxy/Annotation/Endpoint.php

<?php

namespace RemoteProxy\Annotation;

/**
 * @Annotation
 */
class Endpoint
{
    public $path;
    public $method;

    public function __construct($parameters)
    {
        $this->path = $parameters['path'];
        $this->method = isset($parameters['method']) ? $parameters['method'] : 'get';
    }
}

In the above code, we create a property for each parameter of the annotation.

In our case, these are $path and $method. In the constructor, we receive the annotation parameters as an associative array ($parameters). Finally, we assign the elements of $parameters to their respective properties in the class.

Now, we update the interface:

Filename: src/RemoteProxy/LibraryInterface.php

<?php

namespace RemoteProxy;

use RemoteProxy\Annotation\Endpoint;

interface LibraryInterface
{    
    /**
     * Return all books
     *
     * @Endpoint(path="books")
     */
    public function getBooks();

    /**
     * Return book's details
     *
     *
     * @Endpoint(path="books/:id")
     * @param  int $id
     * @return mixed
     */
    public function getBook($id);

    /**
     * Return author of a book
     *
     * @Endpoint(path="books/:id/authors")
     * @param  int $id
     * @return mixed
     */
    public function getAuthors($id);
}

So far, we have the annotation and the interface, but we should parse and pass them to our adapter, which expects an array of mapping information.

To do this, we create a helper class which takes the interface, extracts the information with the help of PHP’s Reflection API and Doctrine’s annotation parser, and returns the result as an array. We can then pass this array to our adapter – just like the array we hard coded in the previous example.

Let’s name the class UriResolver:

Filename: src/RemoteProxy/UriResolver.php

<?php

namespace RemoteProxy;

use Doctrine\Common\Annotations\AnnotationReader;
use RemoteProxy\Annotation\Endpoint;

class UriResolver
{

    /**
     * Annotation reader instance
     *
     * @var Doctrine\Common\Annotation\AnnotationReader
     */
    protected $annotationReader;

    /**
     * Instantiate the URI resolver
     */
    public function __construct()
    {
        $this->annotationReader = new AnnotationReader();
    }

    /**
     * Return an array of mapping information
     *
     * @param string $interface
     *
     * @return array
     */
    public function getMappings($interface)
    {

        $mappings   = [];
        $methods    = (new \ReflectionClass($interface))->getMethods();

        foreach ($methods as $method) {
            $annotations = $this->annotationReader->getMethodAnnotations($method);
            foreach ($annotations as $annotation) {
                if ($annotation instanceof Endpoint) {
                    $mappings[$method->name] = ['path' => $annotation->path, 'method' => $annotation->method];
                }
            }
        }
        return $mappings;
    }
}

In the above code, first, we instantiate Doctrine\Common\Annotation\AnnotationReader and store it in the $annotationReader property.

In getMappings(), we get all the interface’s methods using the reflection API. Then, we iterate over them to parse the DocBlocks. On each iteration, we check if the method has the Endpoint annotation. If it does, we add a new element to the $mappings array, with the method name as the key, and path and method as values.

This is how we use this new class:

Usage example:

<?php

// ...

use ProxyManager\Factory\RemoteObjectFactory;
use RemoteProxy\Adapter\RestAdapter;

$base_uri = 'http://localhost';
AnnotationRegistry::registerLoader('class_exists');
$factory = new RemoteObjectFactory(
            new RestAdapter(
                new \GuzzleHttp\Client([
                    'base_uri' => $base_uri,
                ]),
                // Array
                (new UriResolver())->getMappings(RemoteProxy\LibraryInterface::class)
            )
        );
// ...

You might be wondering about the call to AnnotationRegistry::registerLoader('class_exists'). Since Doctrine annotations are not loaded by the defined PHP autoloaders, we need to use Doctrine’s built-in silent autoloading mechanism:

<?php
AnnotationRegistry::registerLoader('class_exists');

Creating a Special Factory

Every time we want to create a remote object proxy in our code, we need to repeat the above steps which, of course, is inconvenient. In addition, it results in plenty of instantiations across our controllers. As a solution, we abstract the above procedure into a convenient factory class.

We name it RestProxyFactory under the src/RemoteProxy directory:

Filename: src/RemoteProxy/RestProxyFactory.php

<?php

namespace RemoteProxy;

use Doctrine\Common\Annotations\AnnotationRegistry;
use RemoteProxy\Adapter\RestAdapter;

class RestProxyFactory
{
    public static function create($interface, $base_uri)
    {
        // Registering a silent autoloader for the annotation
        AnnotationRegistry::registerLoader('class_exists');

        $factory = new \ProxyManager\Factory\RemoteObjectFactory(
            new RestAdapter(
                new \GuzzleHttp\Client([
                    'base_uri' => rtrim($base_uri, '/') . '/',
                ]),
                (new UriResolver())->getMappings($interface)
            )
        );

        return $factory->createProxy($interface);
    }
}

In the above class, we accept an interface and the API’s base URL (we need this to instantiate Guzzle). Then, we put all the previous instantiation steps in there.

Usage

Now, we can easily create a REST-friendly remote object proxy at any time by using our special factory class.

To test the proxy, let’s define a route in our src/app.php file:

<?php
// src/app.php
// ...

$app->get('test-proxy', function (Application $app) {            

    $proxy = new RestProxyFactory(LibraryInterface::class, 'localhost:9000/api/v1');
    return new JsonResponse([
        'books' => $proxy->getBooks(),
    ]);
});

// ...

In the above code, invoking getBooks() against $proxy, will make an HTTP request to http://localhost:9000/api/v1/books. As a result, we should get the following response:

{
resp: "{"books":"List of books..."}"
}

That’s all there is to it. I hope you enjoyed this article. The complete code is available on Github if you want to try it yourself.

If you just want to use it in your project, you can install it with composer:

composer require lavary/rest-remote-proxy

Wrapping Up

We learned how remote proxies work and how they interact with different remote subjects with different protocols. We also created an adapter suitable for RESTful APIs.

By proxying the remote subjects, we can use them without even knowing they are located in a different address space. We just request, and the proxy makes the calls. It is as simple as that.

How are you using proxies? Do you think this approach might come in handy in some of your own projects?

  • Raúl

    Excelente

  • João Alves

    Very interesting article! What you think would be the best approach to convert the API response into objects?

  • Peter Böthig

    Realy cool stuff for sunday afternoon training . Thank you !

    in app.php this has to be:

    use ApiControllerApiControllerProvider;
    instead of:
    use ProxyControllerApiControllerProvider;

  • Peter Böthig

    There is a bug in the docblock. The first param is path
    /**
    * Compile that selected path
    *
    * @param string $wrappedClass
    * @param string $method
    *
    * @return string Service name
    */
    protected function compilePath($path, $parameters)

  • Peter Böthig

    use ProxyRemoteProxyAdapterRest;

    This class does not exist. The class is named RestAdapter

  • Peter Böthig

    Seems you have refactored all.

    The author methods has to be plural author(s)

    /**
    * Return author of a book
    *
    * @param int $id
    * @return mixed
    */
    public function getAuthor($id);

  • Peter Böthig

    I know, thats just a docubug.

    /**
    *
    *
    */
    public function __construct()
    {
    $this->annotationReader = new AnnotationReader();
    }

  • Peter Böthig

    There is a missing semicolon. This will break the code

    class RestProxyFactory
    {
    public static function create($interface, $base_uri)
    {
    // Registering a silent autoloader for the annotation
    AnnotationRegistry::registerLoader(‘class_exists’);

    $factory = new ProxyManagerFactoryRemoteObjectFactory(
    new RestAdapter(
    new GuzzleHttpClient([
    ‘base_uri’ => rtrim($base_uri, ‘/’) . ‘/’,
    ]),
    (new UriResolver())->getMappings($interface)
    )createProxy($interface);
    }
    }

  • Peter Böthig

    Finally I got it working without the sources in github.

    Seems you have renamed Library in Books !

  • Peter Böthig

    http://localhost:9000/api/v1/books

    This url just return the hardcoded data in the controller.

    When I call /api/v1/test-proxy I only get 404

Recommended
Sponsors
Get the latest in PHP, once a week, for free.