RESTful Remote Object Proxies with ProxyManager
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:
-
Virtual Proxy – for lazily loading the resource-hungry objects, preventing them from occupying memory until they are needed.
-
Protection Proxy – for limiting access to properties or methods of an object through a set of rules.
-
Smart Reference – for adding additional behaviors when a method is called on the subject – suitable for aspect-oriented programming.
-
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?