RESTful Remote Object Proxies with ProxyManager

Share this article

RESTful Remote Object Proxies with ProxyManager

Key Takeaways

  • Remote object proxies in ProxyManager allow seamless interaction with remote services, presenting them as local objects to simplify API integrations.
  • ProxyManager supports various proxy types including virtual proxies for lazy loading, protection proxies for access control, and remote object proxies for interacting with remote services.
  • Custom adapters can be created in ProxyManager to handle specific protocols like RESTful APIs, enhancing flexibility in managing different data communication methods.
  • The use of a custom RESTful adapter involves mapping method calls to specific API endpoints, enabling methodical and organized API requests.
  • ProxyManager utilizes a factory pattern to streamline the creation of proxies, with the help of adapters that manage communication details.
  • Detailed mapping of class methods to API endpoints can be maintained within interfaces using custom annotations, simplifying the configuration and setup of remote object proxies.

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?

Frequently Asked Questions about Restful Remote Object Proxies with ProxyManager

What is the main purpose of using ProxyManager for remote object proxies?

ProxyManager is a powerful tool that allows developers to manage and control remote object proxies in a RESTful manner. It provides a way to create, manipulate, and manage proxies that act as stand-ins for remote objects. This is particularly useful in distributed systems where objects may reside on different servers or even different geographical locations. ProxyManager allows developers to interact with these remote objects as if they were local, simplifying the development process and reducing the complexity of the code.

How does ProxyManager differ from NSXPCConnection in managing remote object proxies?

While both ProxyManager and NSXPCConnection provide mechanisms for managing remote object proxies, they differ in their approach and functionality. NSXPCConnection, a tool provided by Apple, is specifically designed for interprocess communication on macOS and iOS platforms. It allows for the creation of remote object proxies, but its use is limited to Apple’s ecosystem. On the other hand, ProxyManager is a more versatile tool that can be used across different platforms and languages. It provides more advanced features such as lazy loading and ghost objects, making it a more flexible and powerful tool for managing remote object proxies.

Can I use ProxyManager for interprocess communication on macOS or iOS?

Yes, ProxyManager can be used for interprocess communication on any platform that supports PHP, including macOS and iOS. However, it’s important to note that ProxyManager is not specifically designed for interprocess communication like NSXPCConnection. It is a more general tool for managing remote object proxies in a RESTful manner. If you’re working exclusively within the Apple ecosystem, NSXPCConnection might be a more suitable tool. But if you need a more versatile and platform-independent solution, ProxyManager is a great choice.

How does ProxyManager handle lazy loading and ghost objects?

ProxyManager provides built-in support for lazy loading and ghost objects. Lazy loading is a design pattern that delays the loading of an object until it is actually needed. This can significantly improve performance in situations where objects are expensive to create or load. Ghost objects are a type of lazy loading where the object is only partially loaded, with the rest of the object being loaded on demand. ProxyManager provides a simple and intuitive API for managing these advanced features, making it easier for developers to optimize their applications.

What are the benefits of using a RESTful approach with ProxyManager?

A RESTful approach with ProxyManager provides several benefits. It allows for a more intuitive and straightforward way of managing remote object proxies. The stateless nature of REST makes it easier to scale and distribute your application across multiple servers. It also improves interoperability, as REST is a widely adopted standard that can be used with many different languages and platforms. Furthermore, using a RESTful approach with ProxyManager allows for a more modular and decoupled design, making your application easier to develop, test, and maintain.

How does ProxyManager compare to other remote object proxy tools?

ProxyManager stands out for its versatility, advanced features, and RESTful approach. While other tools like NSXPCConnection are platform-specific, ProxyManager can be used across different platforms and languages. It also provides support for advanced features like lazy loading and ghost objects, which are not commonly found in other remote object proxy tools. Furthermore, its RESTful approach makes it more intuitive and easier to use, especially for developers who are already familiar with REST.

Is ProxyManager suitable for large-scale applications?

Yes, ProxyManager is well-suited for large-scale applications. Its support for lazy loading and ghost objects allows it to handle large amounts of data efficiently. The stateless nature of its RESTful approach also makes it easier to scale and distribute your application across multiple servers. Furthermore, its versatility and platform independence make it a great choice for large-scale applications that need to operate across different platforms and environments.

Can I use ProxyManager with other programming languages?

ProxyManager is a PHP library, so it’s primarily designed to be used with PHP. However, its RESTful approach makes it interoperable with many different languages and platforms. As long as the other language or platform supports REST, you should be able to use ProxyManager with it. This makes ProxyManager a versatile tool that can be used in a wide variety of applications and environments.

How does ProxyManager improve the development process?

ProxyManager simplifies the development process by providing a straightforward and intuitive way to manage remote object proxies. It abstracts away the complexities of dealing with remote objects, allowing developers to interact with them as if they were local. This reduces the amount of code needed and makes the code easier to understand and maintain. Furthermore, its support for advanced features like lazy loading and ghost objects allows developers to optimize their applications without having to implement these features from scratch.

What are some common use cases for ProxyManager?

ProxyManager can be used in a wide variety of applications and environments. Some common use cases include distributed systems, where objects may reside on different servers or even different geographical locations; large-scale applications, where efficient handling of large amounts of data is crucial; and applications that need to operate across different platforms and environments, thanks to ProxyManager’s versatility and platform independence.

Reza LavarianReza Lavarian
View Author

A web developer with a solid background in front-end and back-end development, which is what he's been doing for over ten years. He follows two major principles in his everyday work: beauty and simplicity. He believes everyone should learn something new every day.

BrunoSOOPHPPHPphp restfulproxyRESTrestfulrestful apirestful api server
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week