Practical PHP Patterns: Remote Facade
Join the DZone community and get the full member experience.
Join For FreeThe Remote Facade pattern is a specialization of the Facade one which abstracts the interaction with a remote service (which is executed out of the PHP process, or out of the current machine).
PHP applications usually aren't fond of transparent remote invocation, since the remote application is often not usually written in PHP (think of Twitter or Google web services). A Remote Facade provides a unique point of access to an external service (usually a web service, since it is so easy to connect to it with PHP code), with a coarse-grained interface that would not leak as a Proxy object would in some chatty use cases.
Implementation
The Remote Faced may create and manage a bunch of domain objects, which you can use to manipulate the returned data and prepare message to send; only when the Remote Facade is called network communication happens.
Many small objects are the key to a resilient and easy to use object model. The Remote Facade hides the complexity of the communication, which involves mapping of objects to a data stream, and is built over this object model.
Other responsibilities can be applied to the Facade, given its key position on a port of the application.
For example security and access control can be applied by making checks in its methods. Also automatic transactional control can be implemented based on the semantics of the model: every method is a transaction, since you can never rely on the the calling order and consistency of client code.
There are also responsibilities that a Remote Facade should not have. The main one is domain logic: it is a more orthogonal approach to model business logic in an external object model and leave only the translation mechanism in the Remote Facade (that way you would be even able to swap different Facades over the same domain model).
Backends
PHP has networking under its skin, so there are multiple ways to interact with remote servers.
A common approach is using curl (it is implemented as an extension, but it is very common in PHP installations).
The PHP extension is based on libcurl, a C library which according to php.net currently supports http, https, ftp, gopher, telnet, dict, file, and ldap protocols. And also HTTPS certificates, HTTP POST, HTTP PUT, FTP uploading, HTTP form based upload, proxies, cookies, and user+password authentication.
Another standard networking mechanism is using sockets and streams (stream_socket_client() and the related famiily of functions). The definition of stream in PHP is that of a resource (which is a PHP variable type) which exhibits streamable behavior, meaning that it can be written to or read from in a linear fashion. From PHP 4.3, streams unify all these resources under a common interface to reuse various functions orthogonally to the wrappers which identify the various types of sources, like the filesystem, HTTP and FTP, but also compression streams and Phar archives. The stream extension, which includes the set of functions that handle streams (comprehending the remote ones, via sockets), is even more common that the curl one.
Advantages
There are many advantages to insulate your application from external resources using a Remote Facade.
For starters, you will keep network-related code in one place, so that it would be easy to change, cohesive and with no duplication in various parts of the codebase.
You also easily obtain insulation during testing: you can run integration tests on the Facade itself, and unit-level ones on the few classes that use it. Usually domain classes are managed by the Facade for transmission but do not depend on it, so they don't need a fake implementation to be tested, like Entities do not need fake repositories for testing in general.
Disadvantages
It's hard to find a single disadvantage in the usage of this foundational pattern.
The only problem I can think of is that remoting is not clearly expressed in the Api since all the networking code is hidden. Using the Facade carelessly could lead to very bad performance even with the precautions of a coarse interface which depends on domain objects, but it should be clear that if you're calling an external web site the speed of your scripts could be slower than when acting on in-memory objects...
Example
The sample implementation of a Remote Facade I'll present here is the Zend_Service_SlideShare component of Zend Framework: it hides interactions with slideshare.com under an object-oriented interface.
It also has a domain model which contains business logic, via its Zend_Service_SlideShare_SlideShow class. This is one of the many Zend_Service classes which are currently being ported to Zend Framework 2.
Here is the relevant code (the main Api methods plus a protected Factory Method and the constructor), taken from Zend Framework 1.x trunk.
<?php
/**
* The Zend_Service_SlideShare component is used to interface with the
* slideshare.net web server to retrieve slide shows hosted on the web site for
* display or other processing.
*
* @category Zend
* @package Zend_Service
* @subpackage SlideShare
* @throws Zend_Service_SlideShare_Exception
* @copyright Copyright (c) 2005-2010 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/
class Zend_Service_SlideShare
{
/**
* The Constructor
*
* @param string $apikey The API key
* @param string $sharedSecret The shared secret
* @param string $username The username
* @param string $password The password
*/
public function __construct($apikey, $sharedSecret, $username = null, $password = null)
{
$this->setApiKey($apikey)
->setSharedSecret($sharedSecret)
->setUserName($username)
->setPassword($password);
$this->_httpclient = new Zend_Http_Client();
}
...
/**
* Uploads the specified Slide show the the server
*
* @param Zend_Service_SlideShare_SlideShow $ss The slide show object representing the slide show to upload
* @param boolean $make_src_public Determines if the the slide show's source file is public or not upon upload
* @return Zend_Service_SlideShare_SlideShow The passed Slide show object, with the new assigned ID provided
*/
public function uploadSlideShow(Zend_Service_SlideShare_SlideShow $ss, $make_src_public = true)
{
$timestamp = time();
$params = array('api_key' => $this->getApiKey(),
'ts' => $timestamp,
'hash' => sha1($this->getSharedSecret().$timestamp),
'username' => $this->getUserName(),
'password' => $this->getPassword(),
'slideshow_title' => $ss->getTitle());
$description = $ss->getDescription();
$tags = $ss->getTags();
$filename = $ss->getFilename();
if(!file_exists($filename) || !is_readable($filename)) {
require_once 'Zend/Service/SlideShare/Exception.php';
throw new Zend_Service_SlideShare_Exception("Specified Slideshow for upload not found or unreadable");
}
if(!empty($description)) {
$params['slideshow_description'] = $description;
} else {
$params['slideshow_description'] = "";
}
if(!empty($tags)) {
$tmp = array();
foreach($tags as $tag) {
$tmp[] = "\"$tag\"";
}
$params['slideshow_tags'] = implode(' ', $tmp);
} else {
$params['slideshow_tags'] = "";
}
$client = $this->getHttpClient();
$client->setUri(self::SERVICE_UPLOAD_URI);
$client->setParameterPost($params);
$client->setFileUpload($filename, "slideshow_srcfile");
require_once 'Zend/Http/Client/Exception.php';
try {
$response = $client->request('POST');
} catch(Zend_Http_Client_Exception $e) {
require_once 'Zend/Service/SlideShare/Exception.php';
throw new Zend_Service_SlideShare_Exception("Service Request Failed: {$e->getMessage()}", 0, $e);
}
$sxe = simplexml_load_string($response->getBody());
if($sxe->getName() == "SlideShareServiceError") {
$message = (string)$sxe->Message[0];
list($code, $error_str) = explode(':', $message);
require_once 'Zend/Service/SlideShare/Exception.php';
throw new Zend_Service_SlideShare_Exception(trim($error_str), $code);
}
if(!$sxe->getName() == "SlideShowUploaded") {
require_once 'Zend/Service/SlideShare/Exception.php';
throw new Zend_Service_SlideShare_Exception("Unknown XML Respons Received");
}
$ss->setId((int)(string)$sxe->SlideShowID);
return $ss;
}
/**
* Retrieves a slide show's information based on slide show ID
*
* @param int $ss_id The slide show ID
* @return Zend_Service_SlideShare_SlideShow the Slideshow object
*/
public function getSlideShow($ss_id)
{
$timestamp = time();
$params = array('api_key' => $this->getApiKey(),
'ts' => $timestamp,
'hash' => sha1($this->getSharedSecret().$timestamp),
'slideshow_id' => $ss_id);
$cache = $this->getCacheObject();
$cache_key = md5("__zendslideshare_cache_$ss_id");
if(!$retval = $cache->load($cache_key)) {
$client = $this->getHttpClient();
$client->setUri(self::SERVICE_GET_SHOW_URI);
$client->setParameterPost($params);
require_once 'Zend/Http/Client/Exception.php';
try {
$response = $client->request('POST');
} catch(Zend_Http_Client_Exception $e) {
require_once 'Zend/Service/SlideShare/Exception.php';
throw new Zend_Service_SlideShare_Exception("Service Request Failed: {$e->getMessage()}", 0, $e);
}
$sxe = simplexml_load_string($response->getBody());
if($sxe->getName() == "SlideShareServiceError") {
$message = (string)$sxe->Message[0];
list($code, $error_str) = explode(':', $message);
require_once 'Zend/Service/SlideShare/Exception.php';
throw new Zend_Service_SlideShare_Exception(trim($error_str), $code);
}
if(!$sxe->getName() == 'Slideshows') {
require_once 'Zend/Service/SlideShare/Exception.php';
throw new Zend_Service_SlideShare_Exception('Unknown XML Repsonse Received');
}
$retval = $this->_slideShowNodeToObject(clone $sxe->Slideshow[0]);
$cache->save($retval, $cache_key);
}
return $retval;
}
/**
* Converts a SimpleXMLElement object representing a response from the service
* into a Zend_Service_SlideShare_SlideShow object
*
* @param SimpleXMLElement $node The input XML from the slideshare.net service
* @return Zend_Service_SlideShare_SlideShow The resulting object
*/
protected function _slideShowNodeToObject(SimpleXMLElement $node)
{
if($node->getName() == 'Slideshow') {
$ss = new Zend_Service_SlideShare_SlideShow();
$ss->setId((string)$node->ID);
$ss->setDescription((string)$node->Description);
$ss->setEmbedCode((string)$node->EmbedCode);
$ss->setNumViews((string)$node->Views);
$ss->setPermaLink((string)$node->Permalink);
$ss->setStatus((string)$node->Status);
$ss->setStatusDescription((string)$node->StatusDescription);
foreach(explode(",", (string)$node->Tags) as $tag) {
if(!in_array($tag, $ss->getTags())) {
$ss->addTag($tag);
}
}
$ss->setThumbnailUrl((string)$node->Thumbnail);
$ss->setTitle((string)$node->Title);
$ss->setLocation((string)$node->Location);
$ss->setTranscript((string)$node->Transcript);
return $ss;
}
require_once 'Zend/Service/SlideShare/Exception.php';
throw new Zend_Service_SlideShare_Exception("Was not provided the expected XML Node for processing");
}
}
Opinions expressed by DZone contributors are their own.
Comments