View Source

<p>The various server classes in Zend Framework 1 were developed over a number of years. While an attempt was made to keep them consistent (all of them follow the API of PHP's SoapServer class), they diverged on a number of fronts, while still implementing similar functionality.</p>

<p>As examples:</p>

<ul>
<li>Zend_XmlRpc_Server and Zend_Json_Server both each have a way to provide API-like documentation on the services exposed - XML-RPC via the system.* methods, JSON-RPC via SMD. The information they need to communicate is basically identical, with the exception of how types are communicated. AMF has done similarly, with its Adobe_Introspector subcomponent.</li>
<li>All servers need a way to store information on the services attached to them. This information includes callbacks, the name they expose via the server, and signature information. This is primarily done via the Zend_Server_Reflection component, but we have diverged from that (both JSON-RPC and AMF allow different permutations of this).</li>
<li>The AMF server allows a lot more functionality than the other servers &ndash; it provides functionality surrounding automatic scanning of directories for service classes and types, attachment of ACLs based on signatures, and more. As a result, usage of that component is quite different for end users than the other server components.</li>
<li>Several server components utilize functionality such as header() which have side effects that make testing difficult.</li>
</ul>


<p>Additionally, there are problems with some of the functionality. As an example, each relies on Zend_Server_Reflection in order to define the prototypes, which in turn enforces that incoming signatures are valid before proxying on to the underlying callbacks. This has several problems.</p>

<ul>
<li>It is a runtime task, and can be expensive, particularly if you have large numbers of classes or functions you plan to expose.</li>
<li>Serialization of the structure has proved formidable. Often, the structure provided by Zend_Server_Reflection is complex enough that it's faster to regenerate it from Reflection than to build it from the serialized structure.</li>
<li>Difficulty/impossibility of customizing what gets attached. As an example, calling <code>setClass()</code> automatically exposes every public method; this may not be desired (e.g., setters likely should be omitted).</li>
</ul>


<p>At the most fundamental level, each of the components &ndash; Zend_Amf_Server, Zend_Json_Server, Zend_Soap_Server, and Zend_XmlRpc_Server &ndash; are RPC servers, and as such could benefit from a consistent structure and workflow. The architecture should be crafted to be flexible enough to allow developers to customize that workflow (for instance, to hook in ACLs, etc.), without requiring modifying or extending core functionality.</p>

<h2>Proposed Architecture</h2>

<p>I propose the following core architecture for server components.</p>

<ul>
<li><strong>Service</strong>: A class that describes a single RPC callback. It would consist of:
<ul>
<li><strong>name</strong>: the name by which it is exposed by the server</li>
<li><strong>target</strong>: the actual callback that will be invoked. Most likely, this will compose a Zend\Stdlib\CallbackHandler, as this latter handles things like lazy-loading. However, you would be able to pass a callback of any kind here to create it.</li>
<li><strong>signatures</strong>: an array of signatures by which it may be invoked (see the <strong>Signature</strong> item below)</li>
<li><strong>return</strong>: an array of possible return types</li>
<li><strong>description</strong>: an optional description detailing what the service does</li>
<li><strong>toArray()</strong>: provides an array representation of the service. <strong>Note</strong>: if the target contains an object, this may not be possible or suitable.</li>
</ul>
</li>
<li><strong>ServiceAggregate</strong>: A class aggregating one or more <strong>Service</strong> objects. This would expose the following methods:
<ul>
<li><strong>add(Service $service)</strong></li>
<li><strong>remove($nameOrServiceInstance)</strong></li>
<li><strong>exists($name)</strong></li>
<li><strong>get($name)</strong></li>
<li><strong>addFromArray(array $array)</strong>: acts as a factory, and attaches immediately</li>
<li><strong>dump</strong>:</li>
</ul>
</li>
<li><strong>Signature</strong>: <strong>Iterable</strong> class detailing a single valid signature. Aggregates <strong>Param</strong> objects</li>
<li><strong>Param</strong>: Class describing a single method parameter. Consists of:
<ul>
<li><strong>name</strong>: Name of parameter</li>
<li><strong>type</strong>: Type of parameter</li>
<li><strong>order</strong>: Order of parameter within method</li>
<li><strong>optional</strong>: Whether or not the parameter is optional</li>
<li><strong>default value</strong>: Default value for the parameter, if optional</li>
<li><strong>description</strong>: Optional description of the parameter's purpose</li>
<li>Note that a <strong>Param</strong> object only requires a single <strong>type</strong>. This is because for every permutation of types for a given parameter, we should report a different signature, to ensure validity.</li>
</ul>
</li>
<li><strong>Server</strong>: A <strong>Dispatchable</strong> class that composes a <strong>ServerAggregate</strong>. This class would compose an <strong>EventManager</strong> instance.</li>
</ul>


<p>Each server class would have protocol-specific Request and Response objects. These would handle the following tasks:</p>

<ul>
<li>
<ul>
<li>Martialling input. Most protocols have a specific request format required, and the Request object would ensure that it (a) received that format, and (b) it's able to translate it to native PHP structures.</li>
<li>Martialling output. Most protocols have a specific output format, and the Request object would ensure that the result of operations is properlly exposed.</li>
</ul>
</li>
</ul>


<p>Additionally, if the protocol requires it, a &quot;Fault&quot; object would be provided, which would represent an error condition. This object would encapsulate any protocol-specific error codes, and would decorate a Response object.</p>

<p>Finally, the assumption will be that the Request or Response object provided to the <code>dispatch()</code> method may not be of the protocol-specific type. Either the Request/Response objects would allow decorating the original objects, or we would provide a default event listener that could martial the protocol-specific variants from the provided instances.</p>

<p>A convenience method, <code>handle()</code>, would be provided that would automatically martial a Request and Response object for you, and pass them to the <code>dispatch()</code> method.</p>

<ul>
<li><strong>ServiceCompiler</strong>: A suite of classes that act as <strong>ServiceAggregates</strong> and which would be used to generate <strong>Service</strong> instances. These would focus on three primary use cases:
<ul>
<li><strong>Runtime</strong>: Provided one or more classes, objects, or function names, would use Reflection to generate Service and ServiceAggregate objects.</li>
<li><strong>Configuration</strong>: Given an array or array-like data structure (such as Zend\Config), would work as a factory to generate the Service and ServiceAggregate objects.</li>
<li><strong>Manual</strong>: Developer manually creates Service and ServiceAggregate objects.</li>
<li>Combination of the above.</li>
</ul>
</li>
</ul>


<h2>Usage</h2>

<p>Typically, you'll first build your services.</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
// Reflection:
$services = new ReflectionCompiler();
$services->setClass('My\Service\Foo', 'foo'); // under namespace "foo"
$services->addFunction('My\endpoint', 'my'); // under namespace "my"

// Configuration:
$services = new ConfigurationCompiler();
$services->setConfig($config); // An array or Traversable

// Manually:
$signature = new Signature();
$signature->add(new Param('message', 'string', 0));
$service = new Service();
$service->setName('world.hello')
->setTarget(function($message) { return 'Hello, ' . $message; })
->setSignatures(array($signature))
->setReturn(array('string'))
->setDescription('Hello world service');
$services = new ServiceAggregate();
$services->add($service);

// Combination:
$services = new ReflectionCompiler();
$services->setClass('My\Service\Foo', 'foo'); // under namespace "foo"
$services->addFunction('My\endpoint', 'my'); // under namespace "my"

$config = new ConfigurationCompiler();
$config->setConfig($config); // An array or Traversable

foreach ($config as $service) {
$services->add($service);
}
]]></ac:plain-text-body></ac:macro>

<p>Once you have your services ready, you can pass them to your server:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
$server = new Server();
$server->setServices($services);
]]></ac:plain-text-body></ac:macro>

<p>Now we can handle the request:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
$response = $server->handle();
$response->send();
]]></ac:plain-text-body></ac:macro>

<p>If we wanted to add ACL checks, we might do the following:</p>

<ac:macro ac:name="code"><ac:default-parameter>php</ac:default-parameter><ac:plain-text-body><![CDATA[
// Assume $acl and $user are already created
$server->events()->attach('handle', function($e) use ($acl, $user) {
$request = $e->getRequest();
$resource = $request->getMethod();

if ($acl->isAllowed($user, $resource)) {
// Okay to continue;
return;
}

// Failed, so let's return a fault
$fault = new Fault();
$fault->setCode(401);
return $fault;
}, 100); // execute at high priority
]]></ac:plain-text-body></ac:macro>

<h2>Benefits</h2>

<ul>
<li>Shared infrastructure == less maintenance. The only real place the server components would diverge is the request/response implementations.</li>
<li>EventManager would provide a consistent way to alter workflows across protocols, and allow bringing the same capabilities across all implementations.</li>
<li>Separating the service definitions into a common architecture ensures re-usability, and will also make it simpler to create serializable or programmatic definitions sculpted to exactly what you plan to expose.</li>
</ul>