Archive for the ‘View Helpers’ Category

More Doctrine 2 and Zend Framework integration goodies!

Friday, July 15th, 2011

Introduction

So, it’s been a while since I’ve posted but I’ve been hard at work the last few evenings on a few very cool features.

  1. I’ve written a Spiffy\Entity class that you can extend Doctrine 2 entities from to gain a few new features.
    • Zend Validator and Zend Filter annotations for entities.
    • You can now validate directly on the model (this is how it should be, IMO).
  2. I’ve written Spiffy\Form and Spiffy\Dojo\Form that integrates with Spiffy\Entity to do field type detection automatically somewhat similar to Symfony 2.
    • The form is smart enough to read the validators/filters from Spiffy\Entity. No more adding them directly to each form element!
    • You have the option to autoPersist Spiffy\Entity if the form validates.
  3. I’ve added three new form elements – Spiffy_Form_Element_Entity, Spiffy_Dojo_Form_Element_ComboBoxEntity, and Spiffy_Dojo_Form_Element_FilteringSelectEntity.
    • These elements allow you to map ManyToOne relations directly into your form. They’re still a work in progress (need to add store data option to the Dojo elements).
This project is currently in early development and I would not recommend use in a production environment! Follow the GitHub repository README for when I consider it stable.

Usage

Let’s get to the nitty gritty and build a complete registration form, controller, and entity. At the time of writing (7/15/2011) this post is accurate, however, the latest documentation is always available on my GitHub repository.

Setting up the Spiffy library

  1.  by downloading or cloning it into your include_path.
  2. Add the namespace to application.ini: autoloaderNamespaces[] = Spiffy
  3. Add the Spiffy pluginPaths to application.ini: pluginPaths.Spiffy_Application_Resource = “Spiffy/Application/Resource”
  4. Add the Spiffy annotations to Doctrine in your bootstrap: \Doctrine\Common\Annotations\AnnotationRegistr::registerFile(“Spiffy/Doctrine/Annotations/Zend/Validator.php”);
  5. Set the default entity manager for Spiffy\Form. \Spiffy\Form::setDefaultEntityManager($em);

Creating an entity

// My/Entity/User.php
namespace My\Entity;
use Spiffy\Entity;

/**
 * @ORM\Table(name="user")
 * @ORM\Entity
 */
class User extends Entity
{
        /**
         * @var integer $id
         *
         * @ORM\Column(name="id", type="integer")
         * @ORM\Id
         * @ORM\GeneratedValue(strategy="AUTO")
         */
        private $id;

        /**
         * @var string $username
         *
         * @ORM\Column(type="string")
         */
        private $username;

        /**
         * @var string $email
         *
         * @Assert\EmailAddress()
         * @ORM\Column(type="string")
         */
        private $email;

        /**
         * @var string $password
         *
         * @ORM\Column(type="string")
         */
        private $password;

        public function __toString() {
                return "{$this->username}";
        }

        public function getId() {
                return $this->id;
        }

        public function setId($id) {
                $this->id = $id;
        }

        public function getUsername() {
                return $this->username;
        }

        public function setUsername($username) {
                $this->username = $username;
        }

        public function getEmail() {
                return $this->email;
        }

        public function setEmail($email) {
                $this->email = $email;
        }

        public function getPassword() {
                return $this->password;
        }

        public function setPassword($password) {
                $this->password = $password;
        }
}

The Form

// My/Forms/Register.php
namespace My\Forms\Register;
use Spiffy\Dojo\Form;

class Register extends Form
{
        /**
         * (non-PHPdoc)
         * @see Zend_Form::init()
         */
        public function init() {
                $this->setName('register');
                $this->add('username');
                $this->add('email');
                $this->add('password', 'PasswordTextBox');
                $this->add('submit', 'SubmitButton');
        }

        /**
         * (non-PHPdoc)
         * @see Spiffy.Form::getDefaultOptions()
         */
        public function getDefaultOptions() {
                return array('entity' => 'Blitzaroo\Entity\User');
        }
}

The Controller

// application/controllers/RegisterController.php
use My\Forms\Register;

class RegisterController extends Zend_Controller_Action
{
        public function indexAction() {
                $form = new Register();
                $request = $this->getRequest();

                if ($request->isPost()) {
                        if ($form->isValid($request->getPost())) {
                                // success!

                                // user entity
                                $user = $form->getEntity();

                                // outputs the value of $form->username
                                echo $user->getUsername();
                        }
                }

                $this->view->form = $form;
        }
}

Wrap Up

Well, there’s a small example of what you can do with the new classes. This is not an exhaustive example nor have all the features (Filters) been implemented. Enjoy!

Rendering Dojo Stylesheets with a view helper

Tuesday, October 26th, 2010

I’ve been fervently coding Blitzaroo the passed week and I ran into a rather annoying issue when using AJAX with the Zend Framework and Dojo. Take, for example, using AjaxContext to load content for a dijit.layout.TabContainer that houses several dijit.layout.ContentPane(s).

$ajaxContext = $this->_helper->getHelper('AjaxContext');
$ajaxContext->addActionContexts(array(
    'overview' => 'html',
    'events' => 'html',
    'members' => 'html',
    'recruits' => 'html'))->initContext();

Now then, when the getHref() from a ContentPane is called they load the Ajax data which, in this case, happens to be a dojox.grid.DataGrid. The grid renders just fine without Ajax but when Ajax is used ZF fails to load the proper Stylesheets which need to be loaded manually. I thought, no big deal, I’ll just hit up $this->dojo()->{some method to spit out stylesheets} and to my dismay found that the method is protected. Why? I’m not quite sure. I went ahead and wrote a view helper to fix the problem which is merely a copy/paste of the _renderStylesheet() method but without requiring the Dojo container object. If there is another way around the issue I’m all ears!

class My_View_Helper_DojoStylesheets extends Zend_View_Helper_Abstract
{
        public function dojoStylesheets()
        {
                $dojo = $this->view->dojo();
                $isXhtml = $this->view->doctype()->isXhtml();
                
                if ($dojo->useCdn()) {
                        $base = $dojo->getCdnBase() . $dojo->getCdnVersion();
                } else {
                        $base = $dojo->_getLocalRelativePath();
                }
                
                $registeredStylesheets = $dojo->getStylesheetModules();
                foreach ($registeredStylesheets as $stylesheet) {
                        $themeName = substr($stylesheet, strrpos($stylesheet, '.') + 1);
                        $stylesheet = str_replace('.', '/', $stylesheet);
                        $stylesheets[] = $base . '/' . $stylesheet . '/' . $themeName . '.css';
                }
                
                foreach ($dojo->getStylesheets() as $stylesheet) {
                        $stylesheets[] = $stylesheet;
                }
                
                if ($dojo->registerDojoStylesheet()) {
                        $stylesheets[] = $base . '/dojo/resources/dojo.css';
                }
                
                if (empty($stylesheets)) {
                        return '';
                }
                
                array_reverse($stylesheets);
                $style = '';
                
                return $style;
        }
}

Now my {action}.ajax.phtml script is nice and happy.

// call the regular members action page - no need to duplicate code!
echo $this->render('team-manager/members.phtml');

// inlineScript contains all the formatters for the grid
echo $this->inlineScript();

// New view helper to include the data grid CSS files generated by my DataGrid view helper
echo $this->dojoStylesheets();

dataStore and dataGrid view helpers.

Wednesday, March 10th, 2010

Introduction

Grid’s are used everywhere and one thing the Zend Framework really lacks is a useful view helper to quickly generate grids and their stores. I saw that  Matthew Weier O’Phinney had a proposal for a dojox.grid.DataGrid view helper but no work had been done on it. I send him a tweet and learned that he had intended to finish it but is currently too busy to work on it so I wrote a few view helpers to follow his proposal. The dataStore view helper is fairly simplistic but the dataGrid view helper has quite a bit to it. I plan on refactoring the dataGrid view helper to add support for enhancedGrid and treeGrid but I wanted to share what I had done already. Both view helpers follow the use-cases as presented by Matthew in his dojox.grid.DataGrid view helper proposal.

The dataStore() helper

The dataStore() helper provides support for generating programmatic/declarative data stores and requires an id as well as a dojoType.

Usage

dataStore('store', 'dojox.data.QueryReadStore', array('url' => '/path/to/json'));?>

Code

dojo->requireModule($dojoType);
                
                // Programmatic
                if ($this->_useProgrammatic()) {
                        if (!$this->_useProgrammaticNoScript()) {
                                $this->dojo->addJavascript('var ' . $id . ";\n");
                                $js = $id . ' = ' . 'new ' . $dojoType . '(' . Zend_Json::encode($attribs) . ");";
                                $this->dojo->_addZendLoad("function(){{$js}}");
                        }
                        return '';
                }
                
                // Set extra attribs for declarative
                if (!array_key_exists('id', $attribs)) {
                        $attribs['id'] = $id;
                }
                
                if (!array_key_exists('jsId', $attribs)) {
                        $attribs['jsId'] = $id;
                }
                
                if (!array_key_exists('dojoType', $attribs)) {
                        $attribs['dojoType'] = $dojoType;
                }
                
                return '_htmlAttribs($attribs) . ">
\n"; } }

The dataGrid() helper

The dataGrid helper provides support for generating programmatic/declarative dataGrids. Currently, the only supported grid is dojox.grid.DataGrid but I’ll be adding EnhancedGrid and TreeGrid in the future.

Usage

dataStore('store', 'dojox.data.QueryReadStore', array('url' => '/admin/articles/grid.json'));
$grid = $this->dataGrid('myGrid',
        array(
                'selectionMode' => 'none',
                'style' => 'height: 350px; width: 100%;',
                'store' => 'store',
                'fields' => array(
                        array('field' => 'title', 'label' => 'Title', 'options' => array('formatter' => 'titleFormatter', 'width' => '60%')),
                        array('field' => 'published', 'label' => 'Published', 'options' => array('formatter' => 'primaryFormatter', 'width' => '10%')),
                        array('field' => 'updated_at', 'label' => 'Updated', 'options' => array('width' => '15%')),
                        array('field' => 'created_at', 'label' => 'Created', 'options' => array('width' => '15%')))));
?>
scriptCaptureStart('dojo/method', 'onMouseOver', array('args' => 'row'));?>
var g = dojo.byId("grid-menu-item-"+row.rowIndex);
if (g) { dojo.toggleClass(g, "hide-text"); }
scriptCaptureEnd();?>
scriptCaptureStart('dojo/method', 'onMouseOut', array('args' => 'row'));?>
var g = dojo.byId("grid-menu-item-"+row.rowIndex);
if (g) { dojo.toggleClass(g, "hide-text"); }
scriptCaptureEnd();?>

Code

render();
        }
        
        /**
         * DataGrid view helper.
         * 
         * @param string $id JavaScript id for the data store.
         * @param array $attribs Attributes for the data store.
         */
        public function dataGrid($id = '', array $attribs = array())
        {
                if (!$id) {
                        throw new Zend_Exception('Invalid arguments: required jsId.');
                }
                
                if (!array_key_exists('id', $attribs)) {
                        $attribs['id'] = $id;
                }
                
                if (array_key_exists('fields', $attribs)) {
                        foreach ($attribs['fields'] as $f) {
                                $this->addField($f['field'], $f['label'], isset($f['options']) ? $f['options'] : array());
                        }
                        unset($attribs['fields']);
                }
                
                $this->_attribs = $attribs;
                
                return $this;
        }
        
        /**
         * Static setter for theme.
         * @param string $theme Name of the current them.
         */
        public static function setTheme($theme)
        {
                self::$_theme = $theme;
        }
        
        /**
         * Static getter for theme.
         * @return string
         */
        public static function getTheme()
        {
                return self::$_theme;
        }
        
        /**
         * Static setting for script base.
         * @param string $scriptBase Path to the base script directory.
         */
        public static function setScriptBase($scriptBase)
        {
                self::$_scriptBase = $scriptBase;
        }
        
        /**
         * Static getter for script base.
         * @return string
         */
        public static function getScriptBase()
        {
                return self::$_scriptBase;
        }
        
        /**
         * Adds field data.
         * @param string $field Field name.
         * @param string $label Label of the field.
         * $param array $attribs Optional parameters for the field.
         */
        public function addField($field, $label, array $attribs = array())
        {
                $this->_fields[] = array(
                        'label' => $label, 
                        'attribs' => array_merge(array('field' => $field), $attribs));
                return $this;
        }
        
        /**
         * Adds script captures.
         * @param array $data
         */
        public function addScriptCapture(array $script = array())
        {
                if (!array_key_exists('data', $script)) {
                        throw new Zend_Exception('Script data must include keys data and attribs');
                }
                
                $this->_scriptCapture[] = $script;
                return $this;
        }
        
        /**
         * Begins script capturing.
         */
        public function scriptCaptureStart($type, $event, array $attribs = array())
        {
                if ($this->_captureLock) {
                        throw new Zend_Exception('Cannot nest captures.');
                }
                
                $this->_currentScript = array('type' => $type, 'event' => $event, 'attribs' => $attribs);
                
                $this->_captureLock = true;
                ob_start();
                return;
        }
        
        /**
         * Ends script capturing.
         */
        public function scriptCaptureEnd()
        {
                $data = ob_get_clean();
                $this->_captureLock = false;
                
                $this->_currentScript['data'] = $data;
                
                $this->addScriptCapture($this->_currentScript);
                $this->_currentScript = array();
                
                return true;
        }
        
        /**
         * Renders the grid based on programmatic setting.
         */
        public function render()
        {
                $this->dojo->requireModule('dojox.grid.DataGrid');
                
                // Setup the stylesheet base path
                if (null === self::getScriptBase()) {
                        if ($this->dojo->useLocalPath()) {
                                self::setScriptBase($this->dojo->getLocalPath());
                        } else {
                                self::setScriptBase($this->dojo->getCdnBase() . $this->dojo->getCdnVersion());
                        }
                }
                
                $this->dojo->addStylesheet(self::getScriptBase() . '/dojox/grid/resources/Grid.css');
                $this->dojo->addStylesheet(
                        self::getScriptBase() . '/dojox/grid/resources/' . self::getTheme() . 'Grid.css');
                
                // Programmatic
                if ($this->_useProgrammatic()) {
                        if (!$this->_useProgrammaticNoScript()) {
                                $this->_renderJavascript();
                        }
                        return '
'; } return $this->_renderDeclarative(); } /** * Renders a table for declarative grids. */ protected function _renderDeclarative() { if (!array_key_exists('jsId', $this->_attribs)) { $this->_attribs['jsId'] = $this->_attribs['id']; } $table = '_htmlAttribs($this->_attribs) . ">\n"; foreach ($this->_scriptCapture as $s) { $attribs = array_merge($s['attribs'], array('event' => $s['event'], 'type' => $s['type'])); $table .= "\t_htmlAttribs($attribs) . ">\n"; $table .= "\t\t" . $s['data']; $table .= "\t\n"; } $table .= "\t\n"; $table .= "\t\t\n"; foreach ($this->_fields as $f) { $table .= "\t\t\t_htmlAttribs($f['attribs']) . '>' . $f['label'] . "\n"; } $table .= "\t\t\n"; $table .= "\t\n"; $table .= "
\n"; return $table; } /** * Renders javascript for programmatic declaration. */ protected function _renderJavascript() { $tab = $this->_tab; // Grid names $gridName = $this->_attribs['id'] . 'Grid'; $layoutName = $this->_attribs['id'] . 'Layout'; $this->dojo->addJavascript('var ' . $gridName . ";\n"); // Setup layout $js = $tab . 'var ' . $layoutName . " = [\n"; foreach ($this->_fields as $f) { $f['attribs']['name'] = $f['label']; $f['attribs'] = $this->_jsonExpr($f['attribs']); $js .= "{$tab}{$tab}{$tab}" . Zend_Json::encode($f['attribs'], false, array('enableJsonExprFinder' => true)) . ",\n"; } $js = substr($js, 0, -2); $js .= "];\n\n"; // Use expressions for structure, store, formatter, and get $this->_attribs = $this->_jsonExpr($this->_attribs); $this->_attribs['structure'] = new Zend_Json_Expr($layoutName); // Generate grid $js .= $tab . $tab . $gridName . ' = ' . 'new dojox.grid.DataGrid(' . Zend_Json::encode( $this->_attribs, false, array('enableJsonExprFinder' => true)) . "), document.createElement('div');\n"; $js .= $tab . $tab . 'dojo.byId("' . $this->_attribs['id'] . '").appendChild(' . $gridName . '.domNode);' . "\n"; $js .= $tab . $tab . $gridName . ".startup();\n"; // Generate connects for script captures foreach ($this->_scriptCapture as $s) { $s['data'] = trim($s['data'], "\r\n"); $args = isset($s['attribs']['args']) ? $s['attribs']['args'] : ''; $js .= "{$tab}{$tab}dojo.connect({$gridName}, \"{$s['event']}\", function({$args}){{$s['data']}});\n"; } $this->dojo->_addZendLoad("function(){\n{$tab}{$js}\n{$tab}}"); } /** * Parses an array looking for keys that should be Zend_Json_Expr()'s and coverts them. */ protected function _jsonExpr(array $data) { $jsonExprAttr = array('formatter', 'get', 'query', 'store'); foreach ($jsonExprAttr as $exp) { if (array_key_exists($exp, $data)) { $data[$exp] = new Zend_Json_Expr($data[$exp]); } } return $data; } }

Final Thoughts

These view helpers are by no means complete but I will say that I’m currently using them in a production environment with no issues. I tested both declarative/programmatic usage and both functioned as expected. I also wanted to note that I’m by no means intending to step on Matthew’s toes but I know how busy he is with Zend Framework 2.0 (among other things). If anyone has any thoughts or use for this please comment below and, who knows, maybe I’ll make a proposal for it (or take over Matthew’s).