User Tools

Site Tools


php:zf_custom_xmlmode

Create a custom XMLModel (and implement a custom strategy)

2014-07-17

Why?

This tutorial is intended to help newcomers to the (in)famous Zend Framework 2 to put some light into custom ViewModel topic and also custom Strategy. It's maybe already known to the user that the ZF2 documentation is basically a joke, and secondly, in their attempt to make it easier, more flexible and to implement good practices, the ZF2 actually manages to make things extremely complicated in the end.

The question that remains is if it's really worth the trouble to spend that much time diving into the framework, or just go for another one (“imperfect”) but faster to deliver results. The answer is on you, dear reader, but for the moment let's just dive into it.

Oddly enough, ZF2 offers a JsonModel or a FeedModel, but no XmlModel. The purpose of this tutorial is to show you how.

Install ZF2 Skeleton

This is the easiest step, just follow the instructions .

Short description on what we need

In order to have in our code something as:

   ...
   return new XmlModel($response);   

we have to build:

  • XmlModel
  • ViewXmlStrategy
  • XmlRenderer
  • factories for strategy and renderer

Configuration

in module/Application/config/module.config.php add the next section with 2 more strategies:

'view_manager' => array(
         ....
        'strategies' =>  array(
            'ViewXmlStrategy',
            'ViewJsonStrategy'
        ),
)        

The ViewJsonStrategy is already builtin, ready to be used. The other one, ViewXmlStrategy will be the one that we're gonna build.

Next step is…

Factories

the same module.config.php now for factories:

    'service_manager' => array(
        'abstract_factories' => array(
            'Zend\Cache\Service\StorageCacheAbstractServiceFactory',
            'Zend\Log\LoggerAbstractServiceFactory',
        ),
        'aliases' => array(
            'translator' => 'MvcTranslator',
            'ViewXmlStrategy' => 'Utils\View\Strategy\XmlStrategy',
            'ViewXmlRenderer' => 'Utils\View\Renderer\XmlRenderer'
        ),
        'factories' => array(
            'Utils\View\Strategy\XmlStrategy' => 'Utils\Factory\XmlStrategyFactory',
            'Utils\View\Renderer\XmlRenderer' => 'Utils\Factory\XmlRendererFactory'
        ),
    ),

The specified files doesn't exist at this moment. Actually not event the folder structure. So let's create it next in this configuration:

Also, modify your composer.json to make it aware of the new location:

    "autoload": {
        "psr-0": {
            "Utils": "lib/"
        }
    }

Run php composer.phar update after that.

XmlStrategy

in lib/Utils/View/Strategy create a new file called XmlStrategy.php

<?php namespace Utils\View\Strategy;
 
use Zend\EventManager\AbstractListenerAggregate;
use Zend\EventManager\EventManagerInterface;
use Zend\View\ViewEvent;
 
use Utils\View\Model;
 
 
class XmlStrategy extends AbstractListenerAggregate
{
    protected $charset = 'utf-8';
 
    /**
     * @var XmlRenderer
     */
    protected $renderer;
 
    public function __construct($renderer = null)
    {
        $this->renderer = $renderer;
    }
 
    public function attach(EventManagerInterface $events, $priority = 1)
    {
        $this->listeners[] = $events->attach(ViewEvent::EVENT_RENDERER, array($this, 'selectRenderer'), $priority);
        $this->listeners[] = $events->attach(ViewEvent::EVENT_RESPONSE, array($this, 'injectResponse'), $priority);
    }
 
    public function setCharset($charset)
    {
        $this->charset = (string) $charset;
        return $this;
    }
 
    public function getCharset()
    {
        return $this->charset;
    }
 
    public function selectRenderer(ViewEvent $e)
    {
        $model = $e->getModel();
 
        if (!$model instanceof Model\XmlModel) {
            return;
        }
 
        return $this->renderer;
    }
 
 
    public function injectResponse(ViewEvent $e)
    {
        $renderer = $e->getRenderer();
 
        if ($renderer !== $this->renderer) {
            // Discovered renderer is not ours; do nothing
            return;
        }
 
        $result   = $e->getResult();
 
        if (!is_string($result)) {
            // We don't have a string, and thus, no XML
            return;
        }
 
        // Populate response
        $response = $e->getResponse();
        $response->setContent($result);
        $response->getHeaders()->addHeaderLine('Content-Type', 'text/xml; charset='.$this->charset);
    }
}

XmlStrategy Factory

in lib/Utils/Factory/ create a new file called XmlStrategyFactory

<?php namespace Utils\Factory;
 
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
use Utils\View\Strategy\XmlStrategy;
 
class XmlStrategyFactory implements FactoryInterface
{
    /**
     * Create and return the XML view strategy
     *
     * Retrieves the XMLRenderer service from the service locator, and
     * injects it into the constructor for the XML strategy.
     *
     * It then attaches the strategy to the View service, at a priority of 100.
     *
     * @param  ServiceLocatorInterface $serviceLocator
     * @return JsonStrategy
     */
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $xmlRenderer = $serviceLocator->get('ViewXmlRenderer');
        $xmlStrategy = new XmlStrategy($xmlRenderer);
 
        return $xmlStrategy;
    }
}

XmlRenderer

in lib/Utils/View/Renderer/ create a new file XmlRender.php

<?php namespace Utils\View\Renderer;
 
use Zend\View\Exception;
use Utils\View\Model\XmlModel;
use Zend\View\Model\ModelInterface as Model;
use Zend\View\Renderer\RendererInterface as Renderer;
use Zend\View\Resolver\ResolverInterface as Resolver;
 
/**
 * XML renderer
 */
class XmlRenderer implements Renderer
{
    /**
     * @var Resolver
     */
    protected $resolver;
 
 
    public function getEngine()
    {
        return $this;
    }
 
    public function setResolver(Resolver $resolver)
    {
        $this->resolver = $resolver;
    }
 
 
    public function render($nameOrModel, $values = null)
    {
        if ($nameOrModel instanceof Model && $nameOrModel instanceof XmlModel) {
            return $nameOrModel->serialize();
        }
 
        // Both $nameOrModel and $values are populated
        throw new Exception\DomainException(sprintf(
            '%s: Do not know how to handle operation when both $nameOrModel and $values are populated',
            __METHOD__
        ));
    }
}

XmlRenderer Factory

Also, in lib/Utils/Factory a new file XmlRendererFactory.php:

<?php namespace Utils\Factory;
 
 
use Zend\ServiceManager\FactoryInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
 
use Utils\View\Renderer\XmlRenderer;
 
class XmlRendererFactory implements FactoryInterface
{
    public function createService(ServiceLocatorInterface $serviceLocator)
    {
        $xmlRenderer = new XmlRenderer();
        return $xmlRenderer;
    }
}

XmlModel

Finally, the most important part, XmlModel itself (lib/Utils/View/Model):

The code was heavily inspired from here .

<?php namespace Utils\View\Model;
 
use Zend\Http\Response;
use Zend\View\Model\ViewModel;
 
 
class XmlModel extends ViewModel
{
    protected $captureTo = null;
 
    private $xml      = null;
    private $encoding = 'UTF-8';
    private $version  = '1.0';
    private $formatOutput = true;
 
    /**
     * XML is usually terminal
     *
     * @var bool
     */
    protected $terminate = true;
 
 
    public function serialize()
    {
        $vars    = $this->getVariables();
        $options = $this->getOptions();
 
        $key = key($vars);
        $xml = $this->createXML($key, $vars[$key]);
 
        return $xml->saveXML();
    }
 
    public function &createXML($nodeName, $array = array())
    {
        $xml = $this->getXMLRoot();
        $xml->appendChild($this->convert($nodeName, $array));
 
        $this->xml = null;
 
        return $xml;
    }
 
    private function getXMLRoot()
    {
        if (!$this->xml) {
            $this->init();
        }
 
        return $this->xml;
    }
 
    public function init()
    {
        $this->xml = new \DomDocument($this->version, $this->encoding);
        $this->xml->formatOutput = $this->format_output;
    }
 
    private function &convert($nodeName, $array = array())
    {
 
        $xml  = $this->getXMLRoot();
        $node = $xml->createElement($nodeName);
 
        if(is_array($array)) {
            if(isset($array['@attributes'])) {
                foreach($array['@attributes'] as $key => $value) {
                    if(!$this->isValidTagName($key)) {
                        throw new \Exception(
                            'Illegal character in attribute name. attribute: ' . $key . ' in node: ' . $nodeName
                        );
                    }
                    $node->setAttribute($key, $this->bool2str($value));
                }
                unset($array['@attributes']);
            }
 
            if(isset($array['@value'])) {
                $node->appendChild($xml->createTextNode($this->bool2str($array['@value'])));
                unset($array['@value']);
 
                return $node;
            } else {
                if(isset($array['@cdata'])) {
                    $node->appendChild($xml->createCDATASection($this->bool2str($array['@cdata'])));
                    unset($array['@cdata']);
 
                    return $node;
                }
            }
        }
 
        if(is_array($array)) {
            foreach($array as $key => $value) {
                if(!$this->isValidTagName($key)) {
                    throw new \Exception(
                        'Illegal character in tag name. tag: ' . $key . ' in node: ' . $nodeName
                    );
                }
                if(is_array($value) && is_numeric(key($value))) {
                    foreach($value as $k => $v) {
                        $node->appendChild($this->convert($key, $v));
                    }
                } else {
                    $node->appendChild($this->convert($key, $value));
                }
                unset($array[$key]);
            }
        }
 
        if(!is_array($array)) {
            $node->appendChild($xml->createTextNode($this->bool2str($array)));
        }
 
        return $node;
    }
 
    private function isValidTagName($tag)
    {
        $pattern = '/^[a-z_]+[a-z0-9\:\-\.\_]*[^:]*$/i';
 
        return preg_match($pattern, $tag, $matches) && $matches[0] == $tag;
    }
 
    private function bool2str($value)
    {
        $value = $value === true ? 'true' : $value;
        $value = $value === false ? 'false' : $value;
 
        return $value;
    }
}

Testing the whole code

Do it in a dirty way: just modify the controller :D

namespace Application\Controller;
 
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
 
use Utils\View\Model\XmlModel;
 
class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        $books = array("books" => array(
            '@attributes' => array(
                'type' => 'fiction',
                'year' => 2011,
                'bestsellers' => true
            ),
            'book'=> array('1984','Foundation','Stranger in a Strange Land')
        ));
 
        return new XmlModel($books);
        # return new ViewModel(); # this was old method
    }
}

The result should look like:

<books type="fiction" year="2011" bestsellers="true">
   <book>
     1984
   </book>
   <book>
     Foundation
   </book>
   <book>
     Stranger in a Strange Land
   </book>
</books>

Bored to follow the tutorial

Just get me to the code .

Disclaimer

I'm by no means a ZF2 expert. Not even close. That's way probably most of you are gonna find a better way to implement the code. But for all ZF2 newcomers out there, this information might potentially save you couple of hours of headbanging against the wall.

For any comment you might have, drop me a message at lemonsoftware [at] runbox [dot] com. Constructive critique is welcome. Personal frustrations are not.

Peace!

php/zf_custom_xmlmode.txt · Last modified: 2014/07/17 15:20 by admin