Converting your Zend Framework MVC application into an XML webservice using one single plugin

Thijs Feryn writes an excel­lent arti­cle on how to con­vert your entire MVC app or one or more con­trollers into a XML ser­vice. I actu­ally have this one in a pro­duc­tion envi­ron­ment and it works like a charm.

That’s right folks, in this blog post I’ll show you how you can con­vert your entire MVC appli­ca­tion into a REST-style XML web­ser­vice. And I’m not talk­ing about refac­tor­ing tons of code … NO, we’ll plug this option in with­out chang­ing a sin­gle thing to your action controllers.

This post will con­tain a detailed descrip­tion of the con­cepts used. The source could of the plu­gin is also shown and finally how it will look like when using it.

The con­cept

Thanks to those lovely hooks in Zend Frame­work you can sim­ply inter­vene in nearly every aspect of the MVC work­flow. The image below shows you the workflow.

Zend Controller Basics

Zend Con­troller work­flow The hooks I’m talk­ing about are actu­ally just meth­ods that are called by the plu­gin bro­ker sys­tem. They already exist as empty meth­ods in the Zend_Controller_Plugin_Abstract which we inherit from. So we over­ride them in order to hook into the MVC flow.

Alle infor­ma­tion on Zend Frame­work plu­g­ins can be found in the ref­er­ence pages. A lot of you Zend Frame­work experts will now say: “why don’t you just use the con­text switch­ing action helper?”. I could have done that, but this requires mod­i­fy­ing your appli­ca­tion. This post is about plug­ging it in, remember?

How am I doing it then? Well … I hook into the work­flow at “post dis­patch” time, this means that de dis­patcher has already processed al con­trollers and is about to send all about back to the browser by using a Zend Con­troller Response object.

Before the front con­troller sends the out­put back to the browser, we empty the output’s body and add our own con­tent. This con­tent is retrieved from the view object which would nor­mally parse and ren­der the view object. The view has a set of prop­er­ties which are set by the con­trollers. I use the reflec­tion API to get a hold of all these items. Finally I seri­al­ize them and out­put it all.

The plu­gin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
<?php
/**
 * My_Plugin_Xml component
 * Turns an Zend Framework MVC website into an XML webservice
 */
/**
 * My_Plugin_Xml class
 *
 * @author Thijs Feryn <[email protected]>
 */
class My_Plugin_Xml extends Zend_Controller_Plugin_Abstract
{
    /**
     * Stores the front controller
     *
     * @var Zend_Controller_Front
     */
    private $_front;
    /**
     * Stores the XML output in DOMDocument format
     *
     * @var DOMDocument
     */
    private $_xml;
    /**
     * Class constructor
     */
    public function __construct()
    {
        $this->_front = Zend_Controller_Front::getInstance();
        $layout = Zend_Layout::getMvcInstance();
        $layout->disableLayout();
    }
    /**
     * Build DOMDocument to convert output to XML
     *
     * @param mixed $return
     * @param Exception $exception
     * @return string
     */
    private function _getXML($return = null,Exception $exception = null)
    {  
        $this->_xml = new DOMDocument('1.0', 'UTF-8');
        $this->_xml->formatOutput = true;
 
        $responseNode = $this->_xml->createElement('response');
 
        $exceptionNode = $this->_xml->createElement('exception');
        if(null !== $exception && $exception == instanceof( Exception ){
            $exceptionNode->appendChild(
                $this->_xml->createElement('message',
                    $exception->getMessage()
                )
            );
            $exceptionNode->appendChild(
                $this->_xml->createElement('code',
                    $exception->getCode()
                )
            ); 
            $exceptionNode->appendChild(
                $this->_xml->createElement('type',
                    get_class($exception)
                )
            ); 
        }
 
        $responseNode->appendChild($exceptionNode);
        if(null !== $return){
            $responseNode->appendChild(
                $this->_serialize('return',$return)
            );
        } else {
            $responseNode->appendChild(
                $this->_xml->createElement('return')
            );
        }
 
        $this->_xml->appendChild($responseNode);
        return $this->_xml->saveXML();
    }
    /**
     * Modify the HTTP response object
     * Remove the HTML body, replace with XML and change the content-type
     *
     * @param mixed $return
     * @param Exception $exception
     */
    private function _setResponse($return = false,Exception $exception = null)
    {
        $this->getResponse()->setHeader('Content-Type','text/xml; charset=UTF-8');
        $this->getResponse()->clearBody();
        $this->getResponse()->setBody(
            $this->_getXML($return,$exception)
        );         
    }
    /**
     * Serialize a mixed value to XML in DOMElement format
     * This method can be used recursively in case of objects and arrays
     *  
     * @param string $name
     * @param mixed $value
     * @return DOMElement
     */
    private function _serialize($name,$value)
    {
        if(is_array($value)){
            $element = $this->_xml->createElement($name);
            foreach ($value as $k=>$v){
                if(is_numeric($k)){
                    $k = 'item';
                }
                $element->appendChild($this->_serialize($k,$v));
            }
        } elseif(is_object($value)){
            $element = $this->_xml->createElement($name);
            $reflection = new ReflectionObject($value);
            $properties = $reflection->getProperties();
            foreach ($properties as $property){
                if($property->isPublic()){
                    $element->appendChild(
                        $this->_serialize(
                            $property->getName(),
                            $property->getValue($value)
                        )
                    );
                }
            }
        }else{
            $element = $this->_xml->createElement(
                $name,
                (string)$value
            );
        }
        return $element;
    }
    /**
     * preDispatch hook that retrieves if an Exception was thrown in the application
     * If an exception is thrown, the exception is passed to the exception part of the XML output and script execution is terminated
     *
     * @param Zend_Controller_Request_Abstract $request
     */
    public function preDispatch(Zend_Controller_Request_Abstract $request)
    {
        if($this->getResponse()->isException()){
            $exArray = $this->getResponse()->getException();
            $this->_setResponse(null,$exArray[0]);  
            $this->getResponse()->sendResponse();
            exit();
        }
    }
    /**
     * postDispatch hook that serializes the view object to XML by modifying the HTTP response
     * If no exception was thrown script execution continues and the postDispatch method will be called
     *
     * @param Zend_Controller_Request_Abstract $request
     */
    public function postDispatch(Zend_Controller_Request_Abstract $request)
    {
        $view = Zend_Controller_Action_HelperBroker::getExistingHelper('ViewRenderer')->view;
        $this->_setResponse($view); 
    }
}

The plu­gin registration

To acti­vate the plu­gin, just call the reg­is­ter­Plu­gin method from the front con­troller. In my case this is done in my Ini­tial­izer class which is all­ready a plugin.

1
$this->_front->registerPlugin(new My_Plugin_Xml());

The app

This appli­ca­tion has one sin­gle con­troller which is the Index­Con­troller. There are 2 actions:

* Index­Ac­tion: the default action which assigns an object to the view
* Excep­tion­Ac­tion: an action which throws an exception

Pretty sim­ple, pretty basic, but mind the excep­tion: my plu­gin can catch it by hook­ing into the flow at pre­Dis­patch time. At that time we can already deter­mine if the response con­tains an excep­tion. Luck­ily, in this stage the front con­troller hasn’t yet sent addi­tional error out­put to the view.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class IndexController extends Zend_Controller_Action
{
    public function indexAction()
    {
        $obj = new stdClass();
        $obj2 = new stdClass();
        $obj2->c = 'xyz';
        $obj2->d = '123';
        $obj->a = 'abc';
        $obj->b = $obj2;
        $this->view->content= $obj;
    }
    public function exceptionAction()
    {
        throw new Exception('It all goes wrong!');
    }
}

The out­put

index­Ac­tion

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<response>
  <exception/>
  <return>
    <content>
      <a>abc</a>
      <b>
        <c>xyz</c>
        <d>123</d>
      </b>
    </content>
  </return>
</response>

excep­tion­Ac­tion

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<response>
  <exception>
    <message>It all goes wrong!</message>
    <code>0</code>
    <type>Exception</type>
  </exception>
  <return/>
</response>




Tags: , , , ,

Leave a Comment

*

Get Adobe Flash player