Page 6 of 9
Posted: Wed Apr 18, 2007 11:34 am
by Maugrim_The_Reaper
I think my idea was, let's build the render tree and give the nodes the basic capabilities that we want first. Then we can look at the classes and if they are similar to Zend_View we can have multiple extended Zend_Views (Zps_View). If they are not similar then we can hang them off of a Zend_View. But let's determine what we want and build it before we think about integration.
My outlook is a bit different - assuming all Views share the same basic features, then we need Zend_View - helpers, filters, etc. Also it's an expansive interface even if the actual Interface file seems to have been condensed to a few basics. It's not that hard to integrate into (and well, adding another View/Renderer calls for more work

).
Anyways, I took the basic interfaces, did some subclassing, added the Composites and lumped it all together with some unit tests. So there's a tested method - enough to talk about at least. Because of Zend_View's render interface, the composite methods was switched to renderAll(). Can't use the render() method since it's definition differs from what the unit tests called for.
Hopefully the Unit Tests speak for themselves.
http://w3style.co.uk/devnet-projects/pe ... tends/Zps/ for those seeking the Subversion url to read the classes. Need to run off so no time to post them all here. All the View files are in there.
Code: Select all
<?php
/**
* @package Zps_View
* @subpackage UnitTests
*/
/** Zps_View */
require_once 'Zps/View.php';
/** Zps_View_Composite */
require_once 'Zps/View/Composite.php';
/** PHPUnit_Framework_TestCase */
require_once 'PHPUnit/Framework/TestCase.php';
/**
* @package Zps_View
* @subpackage UnitTests
*/
class Zps_View_CompositeTest extends PHPUnit_Framework_TestCase
{
public function setUp()
{
$this->errorReporting = error_reporting();
$this->displayErrors = ini_get('display_errors');
error_reporting(E_ALL | E_STRICT);
ini_set('display_errors', true);
}
public function tearDown()
{
error_reporting($this->errorReporting);
ini_set('display_errors', $this->displayErrors);
}
/**
* 'filename' is a simple stand in for a real template name which feeds
* into each composite or leaf for rendering. We're missing a method
* for sharing the scriptpath across all elements though...
*/
public function testCompositeInstantiationAndInterfaceType()
{
$composite = new Zps_View_Composite('filename');
$this->assertTrue($composite instanceof Zend_View_Interface);
$this->assertTrue($composite instanceof Zps_View_Composite_Interface);
}
public function testCompositeAddView()
{
$composite = new Zps_View_Composite('filename');
$composite->add('header', new Zps_View_Composite('filename'));
$this->assertTrue($composite->hasChildren());
}
public function testCompositeGetView()
{
$composite = new Zps_View_Composite('filename');
$composite->add('header', new Zps_View_Composite('filename'));
$this->assertTrue($composite->getChild('header') instanceof Zps_View_Composite_Interface);
}
public function testCompositeRemoveView()
{
$composite = new Zps_View_Composite('filename');
$composite->add('header', new Zps_View_Composite('filename'));
$composite->remove('header');
$this->assertFalse($composite->hasChildren());
}
public function testCompositeHasView()
{
$composite = new Zps_View_Composite('filename');
$composite->add('header', new Zps_View_Composite('filename'));
$this->assertTrue($composite->has('header'));
}
public function testCompositeTraverseViews()
{
$composite = new Zps_View_Composite('layout.phtml', array('basePath'=>realpath('./Zps/View/_files')));
$composite->add('header', new Zps_View_Composite('header.phtml'));
$composite->add('body', new Zps_View_Composite('body.phtml'));
$composite->add('footer', new Zps_View_Composite('footer.phtml'));
$response = $composite->renderAll();
$expectedResponse = file_get_contents(realpath('./Zps/View/_files/scripts/header.phtml'));
$expectedResponse .= file_get_contents(realpath('./Zps/View/_files/scripts/body.phtml'));
$expectedResponse .= file_get_contents(realpath('./Zps/View/_files/scripts/footer.phtml'));
$this->assertEquals($expectedResponse, $response);
}
}
There was one convention added - all composites/leafs are given a "label" (all except the parent View). The labels match the node name, and the View variable output is set to (and hence the variable used in the layout templates). The last test is just simple concatenation of three HTML files we had for the "Hello, World!" example for the zfPetShop's index page.
Posted: Wed Apr 18, 2007 11:44 am
by Maugrim_The_Reaper
Just noting this is the bare minimum for a Composite View - it's all explicitly defined in PHP.
Looking forward to a discussion on where to take it from here...
Posted: Wed Apr 18, 2007 3:22 pm
by Christopher
I need some time to get all of your code running so I can go through it.
Posted: Thu Apr 19, 2007 5:59 am
by Maugrim_The_Reaper
arborint wrote:I need some time to get all of your code running so I can go through it.
No problem

. I won't be making any other changes until you have time to review it.
I think the interface is workable, but it's simplicity is also a weakness. To fully utilise the View classes there needs to be a method of configuring each in turn (e.g. assigning script paths, setting the parent View so children can access it's Model, etc.). At the moment the add() method just sets each new Zps_View's script path (inherited from the parent via Zend_View_Abstract::getScriptPath()). Once there's more than one Module in an application that will cease to be sufficient since each View may need access to very specific Module paths the parent View is unaware of.
Posted: Thu Apr 19, 2007 1:25 pm
by Christopher
I haven't had time and my not until Friday. I noticed you asked about this on the Zend list and got the answer we supected -- it can't be done cleanly with the current View. I enjoyed the gymnastics involved in the dispatchLoopShutdown() plugin code posted. You'd think the smell of that and the "View Helper" solution proposed would have they refactoring the View, but they are in patch-or-ignore-the-cracks mode.
Posted: Fri Apr 20, 2007 3:14 am
by Maugrim_The_Reaper
It just sounds like they're boxed in by the idea of having to push the Model into Views, rather than simply letting View pull in the Model via View Helpers. I'm sort of surprised this hasn't popped up before... I guess the problem is simple lack of use - I don't know of many applications beyond simple ones using the ZF because folk have been waiting for it to become stable. That's enough to keep a lot use cases hidden unless you actively look for them - which is basically what we've gone and done...
Posted: Fri Apr 20, 2007 6:36 am
by Maugrim_The_Reaper
Added an alternate, template based implementation (added a Tmp namespace to separate both). Here the compositing is determined by the templates themselves, rather than an external agent.
zfPetShop/extends/Tmp/View.php:
Code: Select all
<?php
/** Zend_View */
require_once 'Zend/View.php';
/**
* Zps_View_Interface
*/
require_once 'Tmp/View/Interface.php';
class Tmp_View extends Zend_View implements Tmp_View_Interface
{
/**
* Holds the base path for all Modules.
*
* @var string
*/
protected $_baseModulePath = null;
/**
* Constructor
*
*/
public function __construct($config = array())
{
if (array_key_exists('baseModulePath', $config)) {
$this->setBaseModulePath($config['baseModulePath']);
}
parent::__construct($config);
}
/**
* Set the baseModulePath.
*
*/
public function setBaseModulePath($path)
{
// to add validation of path
$this->_baseModulePath = $path;
}
/**
* Retrieve the baseModulePath.
*
*/
public function getBaseModulePath()
{
return $this->_baseModulePath;
}
/**
* Includes a sub-View by reference to the template filename
* and the Module it's located in assuming the recommended
* basePath is adhered to as a convention.
*
* @todo address independent configuration of sub-Views
* @return string
*/
public function attach($file, $module = null)
{
if (!is_null($module)) {
$subView = new Tmp_View;
$basePath = $this->_baseModulePath . DIRECTORY_SEPARATOR . $module . DIRECTORY_SEPARATOR . 'views';
$subView->setBasePath($basePath);
$subView->setMainView($this->getMainView());
} else {
$subView = clone $this;
}
return $subView->render($file);
}
/**
* Set a main parent View sourced from each Request's controller.
*
*/
public function setMainView(Zend_View_Interface $view)
{
$this->_mainView = $view;
}
/**
* Get the main parent View.
*
*/
public function getMainView()
{
if (!isset($this->_mainView)) {
return $this;
}
return $this->_mainView;
}
/**
* Method to clone this View assuming the sub-View (the clone) is from
* the same Module as the original.
* Here we are simply getting rid of the inherited public variables which
* represent the ancestor View's model.
*/
public function __clone()
{
foreach(get_object_vars($this) as $key=>$value) {
$this->__unset($key);
}
}
}
An example template (using sub-Views from the same Module)...
layout.phtml
Code: Select all
<?php echo $this->attach('header.phtml') ?>
<?php echo $this->attach('body.phtml') ?>
<?php echo $this->attach('footer.phtml') ?>
You can see the nesting again in the body.phtml template:
Code: Select all
<?php echo $this->attach('helloworld.phtml') ?>
<?php echo $this->attach('objective.phtml') ?>
Again this all just concatenates a few simple HTML files (no other variables).[/syntax]
Posted: Sat Apr 21, 2007 1:19 pm
by Christopher
Ok, this is only a partial response because I have not had the time to really think through it. I downloaded your code from SVN and have it running. There are a number of things going on, but I want to comment on a few.
1. The fact that you have jettisoned all that filter and helper support code in your View is great. If people want that stuff then there should be a class that extends the base View class. Definitely going in the right direction.
2. A general comment, but there seem to be a lot of mucking around with determining paths, the current module, etc. in the ZF, but not any good support for this. We should think about a clean way for this information to get from the Front Controller to the Action Controller to the Views/Models/Templates. Some way to get rid of the gymnastics you do in Tmp_View::attach().
3. There are lots of good things going on, but you have spread them across Zps_View, Zps_View_Composite, and Tmp_View. I know you are experimenting, but I think the need to be consolidated.
4. I think my big question is how all this functionality should be divided between the View, Response and Template classes?
- The Template should be focused on doing a specific transform to create content
- The View should be focused on presentation logic and managing Templates.
- The Response should be focused on headers and page building -- everything outside the actual content which it gets from the View
Posted: Sun Apr 22, 2007 9:53 am
by Maugrim_The_Reaper
Tmp and Zps are two examples. Zps which puts the compositing in the object family and Tmp which puts the compositing within the templates themselves. The second removes the need to configure objects outside the templates - the template nesting drives View nesting which means there's no configuration needed to build an object tree as Zps might need.
The problem with both is that the ZF allows for a Modular controller/view structure which means any View from each Module has a specific path for helper/filter/scripts. In order to allow Views from different Modules to be nested this base path needs to be set correctly for each View object instance. A use case, there's a template in the "default" Module which needs to grab a sub-View from a "Blog" modules (let's say an article page wishing to display RSS headlines from a related Blog part of the app). The default modules is unaware of the Blog Views and its RSS View Helper. By default the RSS View Helper's part isn't set on the current Main View (so it needs to be set lazily on the sub-View).
Code: Select all
<html>
<body>
<div class="article-content">
My article text...
</div>
<div class="rss-headlines">
$this->attach('rssheadlines.phtml', "Blog");
</div>
</body>
</html>
What's happened here is that the Main View is a class of type Tmp_View (extends Zend_View). The attach() method will create a second Tmp_View object specific to the Blog Module (hence the optional second param). This new View needs to know where to find the RSS View Helper so it can query an RSS Model. This means we need to set the Blog Module's /view path using setBasePath(). This is where is gets tricky - how to get that path? At the moment I just made an assumption (never healthy

) and calculated it based on the "default" Modules path (assumption: default and Blog have the same parent directory so they're paths are similar). This is upheld by allowing for a new configuration option - baseModulePath (see the constructor in Tmp_View). It's imperfect since it forces Modules to all locate in the parent directory, and follow the same basePath convention - but it's a sacrifice to keep attach() as simple as possible in setting paths for a sub-Views helper/filter/script locations.
The rssheadlines.phtml template could be something like (ignoring ZF view helper usage, and not optimising):
Code: Select all
<?php $feed = new Tmp_View_Helper_Rss('http://www.planet-php.net/rss/'); ?>
<ul>
<?php foreach($feed->getTitles(5) as $title): ?>
<li><?php echo $title ?></li>
<?php endforeach; ?>
</ul>
Could just let the ZF convention create a rss() function which returns an instance instead of direct instantiation.
The View Helper class for RSS (assumes no caching so it works out of the box without needing a cache layer):
Code: Select all
require_once 'Zend/Uri.php';
require_once 'Zend/Feed/Rss.php';
class Tmp_View_Helper_Rss
{
protected $_channel = null;
public function __construct($url)
{
if (!Zend_Uri::check($url)) {
throw new Exception('RSS URL is invalid!');
}
$this->_channel = new Zend_Feed_Rss($url);
}
public function getTitles($number)
{
$titles = array();
$count = 0;
$number = intval($number);
// Zend_Feed_Abstract implements Iterator!
while ($count <= $number && $this->_channel->valid()) {
$titles[] = $this->_channel->current()->title();
$count++;
$this->_channel->next();
}
return $titles;
}
}
Hopefully makes sense - I'm falling asleep here (late night yesterday) so my brains a little slow today...

Posted: Sun Apr 22, 2007 12:12 pm
by Christopher
I guess I am wondering why ZF went down this path of trying to be smart about figuring out the paths. I think they may have developed an elaborate solution to something that was not a problem. It is convenience for convenience's sake -- but it really limits flexibility. Like your Blog RSS example, if you want to be able to build your response using common elements then they would not be in that specific View's directory.
I might go so far as to say that we should just hard code these relative paths to the files from some agreed upon point. Is there ever a case where you don't know what module you are in? The Front and Action Controllers seem to be going to great length to calculate and convey to you something you already know.
If we hard code more of the relative path then we lose a little convenience, but we gain the flexiblity to access whatever we need easily.
Posted: Sun Apr 22, 2007 2:10 pm
by Maugrim_The_Reaper
I guess I am wondering why ZF went down this path of trying to be smart about figuring out the paths.
I don't know, I just assume they found the flexibility was needed as the framework progressed and tacked it on. It does show that nesting Zend_View wasn't fully considered so when Modularisation was added it turned subclassing into a harder chore. Looking at that Tmp attach() method to Tmp_View...
Code: Select all
public function attach($file, $module = null)
{
if (!ctype_alnum($module)) {
require_once 'Tmp/View/Exception.php';
throw new Tmp_View_Exception('Invalid module name; must only contain alphanumeric characters');
}
if (!is_null($module)) {
$subView = new Tmp_View;
$basePath = $this->getBaseModulePath() . DIRECTORY_SEPARATOR . $module . DIRECTORY_SEPARATOR . 'views';
$subView->setBasePath($basePath);
$subView->setMainView($this->getMainView());
$subView->setBaseModulePath($this->getBaseModulePath());
} else {
$subView = clone $this;
}
return $subView->render($file);
}
...half the method is ensuring Views inherit the correct paths to look for it's own Module's scripts/helpers/filters. So attach() is handling configuration of new View objects. It should be in a different method altogether for attach() to delegate to. I can't see a way out of it right now - setBasePath() must be called on other Module View objects for them to work at a minimum. Where the View is from the same Module I just went with cloning.
Of course all this is convention - should we configure all Module/View paths up front? Stick them in config file and have a proper Factory method to configure Views on that basis. Then attach() would just reference the Factory method, or perform same Module cloning.[/quote]
Posted: Sun Apr 22, 2007 3:21 pm
by Christopher
I think my point is that this is a mess of code:
Code: Select all
$subView = new Tmp_View;
$basePath = $this->getBaseModulePath() . DIRECTORY_SEPARATOR . $module . DIRECTORY_SEPARATOR . 'views';
$subView->setBasePath($basePath);
$subView->setMainView($this->getMainView());
$subView->setBaseModulePath($this->getBaseModulePath());
To just do this (which has already run similar code to figure out the same stuff):
It is especially annoying because not only do I know exactly where my template is, but all this magic makes it really hard to get to things (like shared display components) that aren't where Zend thinks they should go.
Posted: Sun Apr 22, 2007 3:50 pm
by Maugrim_The_Reaper
Yep, it's messy. But the problem with just using the relative path is that it won't be accepted by the framework's structure - it's not built to handle explicit pathing since everything needs to be defined up front shortly after instantiating objects. Module paths are given to the FC but the View's need to be given the same information separately (a little duplication of tasks).
For starters, it would be found invalid by Zend_Abstract::_script() which enforces a rule that paths must be defined before calling render(), for the others it doesn't offer a way of accessing Module specific helpers/filters if they were needed (helpers can be worked around by direct instantiation of by using a new helper loading method), also how would sub-Views be able to access the Main View's model as set by the Controller?
Agreed it's messy code, but it's messy code with a purpose. Doing it the other way would eventually need messy code in another part of the subclass to accomplish the above. Damned if you do, damned if you don't scenario.
Posted: Sun Apr 22, 2007 7:10 pm
by Christopher
Maugrim_The_Reaper wrote:Yep, it's messy. But the problem with just using the relative path is that it won't be accepted by the framework's structure - it's not built to handle explicit pathing since everything needs to be defined up front shortly after instantiating objects. Module paths are given to the FC but the View's need to be given the same information separately (a little duplication of tasks).
That is not exactly true. The Front Controller needs to sort this stuff out to find "default/employee/list", but the list() method of the Employee controller would have to pretend to be stupid to not know what it is. Why act stupid and rely on someone else to tell you what your name is?
Maugrim_The_Reaper wrote:For starters, it would be found invalid by Zend_Abstract::_script() which enforces a rule that paths must be defined before calling render(), for the others it doesn't offer a way of accessing Module specific helpers/filters if they were needed (helpers can be worked around by direct instantiation of by using a new helper loading method), also how would sub-Views be able to access the Main View's model as set by the Controller?
Why worry about a render() function that does not do what we want anyway? I think finding Models is a different story because Models are often not named in an Action Controller fashion -- they are in a different layer and have names appropriate to that layer.
Maugrim_The_Reaper wrote:Agreed it's messy code, but it's messy code with a purpose. Doing it the other way would eventually need messy code in another part of the subclass to accomplish the above. Damned if you do, damned if you don't scenario.
I am not sure. I think it may be a little less convenient to set the defaults semi-manually, but the benefit is that hopefully we can also specify any shared path we need as well.
Thinking a little further after writing the above. Part of this conversation is exploring how ZF does things to see if we can make it work in a way that we think is proper. It follows that if we can't then we could code around, using existing interfaces when possible. One of the big questions is about the directory structure the ZF uses. Like the ZF, I use separate controller and view directory trees. However keeping files together in a component fashion may make more sense -- given the issues that we have been discussing.
Posted: Tue Apr 24, 2007 11:37 am
by Maugrim_The_Reaper
arborint wrote:Thinking a little further after writing the above. Part of this conversation is exploring how ZF does things to see if we can make it work in a way that we think is proper. It follows that if we can't then we could code around, using existing interfaces when possible. One of the big questions is about the directory structure the ZF uses. Like the ZF, I use separate controller and view directory trees. However keeping files together in a component fashion may make more sense -- given the issues that we have been discussing.
I'm not sure - components can make sense but I tend to prefer the split trees (less directories to navigate). I might look at all this again later. I know there's two current examples in svn - Zps and then the template driven Tmp families, and I'm finding the Tmp one a bit easier to go along with for now.
I think with all the options we're pointing at it's not really nailing down the core problem - creating new View instances to play with and being able to create them in flexible ways. Each View needs to access its own helpers/filters (or not), meet our path requirements (or not), and work with a clean generic attach() method.
I'm thinking I should just go add a static factory() method to clean up the attach() method and then let the factory() sort out the mess. If we want to use various strategies for implementing the View creation we can focus on the Factory, and override that _script() method blocking your paths more friendly for that purpose. If it's more flexible can just add a View factory class and everyone can then subclass it to death for their preferences. Basically, anything to get it out of attach() where it's a meltdown waiting to happen.
As for what to use for the Pet Shop here - need a requirement list to drive a choice.
View helper support is iffy - adding it in the ZF sense works but adds another config layer to Views (Views need to know the path, and the classname prefix to load them automatically...ugh). Filters likely not needed yet. The script paths are needed (whether configured, or handled by passing the path to render() - see Factory suggestion). Views might need a reference to the Main parent View - assuming they need to access that View's model themselves.
I'll stop droning on for now... I suppose I should find a better name for attach() besides to describe what it really does. We also should offer a heavier controller dispatch method useable from a View. Would rarely see use anyway.