Fixes #149
[akelos.git] / lib / AkActionView.php
blob2a7c5af4037f9262135301042b134505259234e9
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4 // +----------------------------------------------------------------------+
5 // | Akelos Framework - http://www.akelos.org |
6 // +----------------------------------------------------------------------+
7 // | Copyright (c) 2002-2006, Akelos Media, S.L. & Bermi Ferrer Martinez |
8 // | Released under the GNU Lesser General Public License, see LICENSE.txt|
9 // +----------------------------------------------------------------------+
11 /**
12 * @package ActionView
13 * @subpackage Base
14 * @author Bermi Ferrer <bermi a.t akelos c.om>
15 * @copyright Copyright (c) 2002-2006, Akelos Media, S.L. http://www.akelos.org
16 * @license GNU Lesser General Public License <http://www.gnu.org/copyleft/lesser.html>
19 /**
20 * Action View templates can be written in two ways. If the template file has a +.tpl+ extension then it uses PHP.
22 * = PHP
24 * You trigger PHP by using embeddings such as <? ?>, <?php ?> and <?= ?>. The difference is whether you want output or not. Consider the
25 * following loop for names:
27 * <b>Names of all the people</b>
28 * <?php foreach($people as $person) : ?>
29 * Name: <?=$person->name ?><br/>
30 * <?php endforeach ?>
32 * == Using sub templates
34 * Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The
35 * classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts):
37 * <?= $controller->render("shared/header") ?>
38 * Something really specific and terrific
39 * <?= $controller->render("shared/footer") ?>
41 * As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the
42 * result of the rendering. The output embedding writes it to the current template.
44 * But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance
45 * variables defined using the regular embedding tags. Like this:
47 * <?php $shared->page_title = "A Wonderful Hello" ?>
48 * <?= $controller->render("shared/header") ?>
50 * Now the header can pick up on the $page_title variable and use it for outputting a title tag:
52 * <title><?= $page_title ?></title>
54 * == Passing local variables to sub templates
56 * You can pass local variables to sub templates by using an array with the variable names as keys and the objects as values:
58 * <?= $controller->render("shared/header", array('headline'=>'Welcome','person'=> $person )) ?>
60 * These can now be accessed in shared/header with:
62 * Headline: <?= $headline ?>
63 * First name: <?= $person->first_name ?>
66 * == JavaScriptGenerator ==
68 * @todo Fully implement Javascript Generators
70 * JavaScriptGenerator templates end in +.js.tpl+. Unlike conventional templates which are used to
71 * render the results of an action, these templates generate instructions on how to modify an already rendered page. This makes it easy to
72 * modify multiple elements on your page in one declarative Ajax response. Actions with these templates are called in the background with Ajax
73 * and make updates to the page where the request originated from.
75 * An instance of the JavaScriptGenerator object named +page+ is automatically made available to your template,
76 * which is implicitly wrapped in an AkActionView/Helpers/PrototypeHelper::update_page method.
78 * When an .js.tpl action is called with +linkToRemote+, the generated JavaScript is automatically evaluated. Example:
80 * linkToRemote(array('url' => array('action' => 'delete')));
82 * The subsequently rendered +delete.js.tpl+ might look like:
84 * <% replace_html 'sidebar', :partial => 'sidebar' %>
85 * <% remove "person-#{person.id}" %>
86 * <% visual_effect :highlight, 'user-list' %>
88 * This refreshes the sidebar, removes a person element and highlights the user list.
90 * See the AkActionView/Helpers/PrototypeHelper/JavaScriptGenerator documentation for more details.
92 class AkActionView extends AkObject
94 var $first_render, $base_path, $assigns, $template_extension, $controller,
95 $logger, $params, $request, $response, $session, $headers, $flash;
96 var $_template_handlers = array();
97 var $template_args = array();
101 function _loadHelpers($helper_dir = null)
103 $helper_dir = empty($helper_dir) ? AK_LIB_DIR.DS.'Helpers' : $helper_dir;
104 if (!empty($this->helpers)){
105 $this->helpers = is_array($this->helpers) ? $this->helpers : array($this->helpers);
106 foreach ($this->helpers as $helper_name){
107 if(empty($this->_helperInstances[$helper_name])){
108 $helper_file_name = $helper_dir.DS.AkInflector::underscore($helper_name).'.php';
109 $helper_class_name = ucfirst($helper_name).'Helper';
110 if (file_exists($helper_file_name)){
111 require_once($helper_file_name);
112 if(class_exists($helper_class_name) === true){
113 $this->_helperInstances[$helper_name] =& new $helper_class_name(&$this);
122 * Register a class that knows how to handle template files with the given
123 * extension. This can be used to implement new template types.
124 * The constructor for the class must take the AkActionView instance
125 * as a parameter, and the class must implement a "render" method that
126 * takes the contents of the template to render as well as the array of
127 * local assigns available to the template. The "render" method ought to
128 * return the rendered template as a string.
130 function _registerTemplateHandler($extension, $className)
132 $this->_template_handlers[$extension] = $className;
135 function AkActionView($base_path = null, $assigns_for_first_render = array(), $controller = null)
137 $this->base_path = empty($base_path) ? AK_VIEWS_DIR : $base_path;
138 $this->assigns = $assigns_for_first_render;
139 $this->assigns_added = null;
140 $this->controller = $controller;
141 $this->logger = !empty($controller) && !empty($controller->Logger);
145 * Renders the template present at <tt>template_path</tt>. If <tt>use_full_path</tt> is set to true,
146 * it's relative to the template_root, otherwise it's absolute. The array in <tt>local_assigns</tt>
147 * is made available as local variables.
149 function renderFile($template_path, $use_full_path = true, $local_assigns = array())
151 if(empty($this->first_render)){
152 $this->first_render = $template_path;
155 $template_path = substr($template_path,0,7) === 'layouts' ? AK_VIEWS_DIR.DS.$template_path.'.tpl' : $template_path;
157 if(!$use_full_path && strstr($template_path,'.')){
158 $template_file_name = $template_path;
159 $template_extension = substr($template_path,strpos($template_path,'.')+1);
160 }else{
161 $template_extension = $this->pickTemplateExtension($template_path);
162 $template_file_name = $this->getFullTemplatePath($template_path, $template_extension);
165 return $this->renderTemplate($template_extension, null, $template_file_name, $local_assigns);
168 * Renders the template present at <tt>template_path</tt> (relative to the template_root).
169 * The array in <tt>local_assigns</tt> is made available as local variables.
171 function render($options = array())
173 if(is_string($options)){
174 return $this->renderFile($options, true);
175 }elseif(is_array($options)){
176 $options['locals'] = empty($options['locals']) ? array() : $options['locals'];
177 $options['use_full_path'] = empty($options['use_full_path']) ? true : false;
179 if (!empty($options['file'])){
180 return $this->renderFile($options['file'], $options['use_full_path'], $options['locals']);
181 }elseif (!empty($options['partial']) && !empty($options['collection'])){
182 return $this->renderPartialCollection($options['partial'], $options['collection'], @$options['spacer_template'], @$options['locals']);
183 }elseif (!empty($options['partial'])){
184 return $this->renderPartial($options['partial'], @$options['object'], @$options['locals']);
185 }elseif ($options['inline']){
186 return $this->_renderTemplate(empty($options['type']) ? 'tpl.php' : $options['type'], $options['inline'], null, empty($options['locals']) ? array() : $options['locals']);
192 * Renders the +template+ which is given as a string as tpl.php or js.tpl depending on <tt>template_extension</tt>.
193 * The array in <tt>local_assigns</tt> is made available as local variables.
195 function renderTemplate($____template_extension, $____template, $____file_path = null, $____local_assigns = array(), $____save_content_in_attribute_as = 'layout')
197 $____local_assigns = array_merge(array_merge($this->_getGlobals(),(array)@$this->assigns,array_merge((array)@$this->_local_assigns,
198 array_merge((array)$____local_assigns,array('controller_name' => $this->_controllerInstance->getControllerName(),'controller' => &$this->_controllerInstance)))));
200 if(!empty($this->_template_handlers[$____template_extension])){
201 $____handler =& $this->_template_handlers[$____template_extension];
202 $____template = empty($____template) ? $this->_readTemplateFile($____file_path) : $____template;
203 $____result = $this->_delegateRender(&$____handler, $____template, $____local_assigns, $____file_path);
204 if(is_array($____result)){
205 $____save_content_in_attribute_as = $____result[0];
206 $____result = $____result[1];
208 }else{
209 trigger_error(Ak::t('Could not find a template engine for delegating templates with the extension %extension',array('%extension'=>$____template_extension)), E_USER_ERROR);
211 $this->{'content_for_'.$____save_content_in_attribute_as} = $____result;
213 return $____result;
216 function addSharedAttributes(&$local_assigns)
218 $this->_local_assigns =& $local_assigns;
221 function pickTemplateExtension($template_path)
223 if($match = $this->delegateTemplateExists($template_path)){
224 return $match;
225 }elseif($this->_templateExists($template_path,'tpl.php')){
226 return 'tpl.php';
227 }elseif($this->_templateExists($template_path, 'js.tpl')){
228 return 'js.tpl';
229 }else{
230 trigger_error(Ak::t('No tpl.php, js.tpl or delegate template found for %template_path',array('%template_path'=>$template_path)), E_USER_ERROR);
231 return false;
235 function delegateTemplateExists($template_path)
237 foreach (array_keys($this->_template_handlers) as $k){
238 if($this->_templateExists($template_path, $k)){
239 return $k;
242 return false;
246 * Returns true is the file may be rendered implicitly.
248 function fileIsPublic($template_path)
250 return strpos(strrchr($template_path,DS),'_') !== 1;
253 function getFullTemplatePath($template_path, $extension)
255 $template_path = substr($template_path,-1*strlen($extension)) == $extension ? $template_path : $template_path.'.'.$extension;
256 return substr($template_path,0,strlen(AK_VIEWS_DIR)) == AK_VIEWS_DIR ? $template_path : $this->base_path.DS.$template_path;
259 function _templateExists($template_path, $extension)
261 $file_path = $this->getFullTemplatePath($template_path, $extension);
263 return !empty($this->_method_names[$file_path]) || file_exists($file_path);
267 * This method reads a template file.
269 function _readTemplateFile($template_path)
271 return Ak::file_get_contents($template_path);
274 function evaluateAssigns()
276 if(empty($this->assigns_added)){
277 $this->_assignVariablesFromController();
278 $this->assigns_added = true;
282 function _delegateRender($handler, $template, $local_assigns, $file_path)
284 $HandlerInstance = new $handler($this);
285 return $HandlerInstance->render($template, $local_assigns, $file_path);
288 function _assignVariablesFromController()
290 foreach ($this->assigns as $k=>$v){
291 $this->$k = $v;
295 function _javascriptTemplateExists($template_path)
297 return $this->_templateExists($template_path,'js.tpl');
301 * Partial Views
303 * There's also a convenience method for rendering sub templates within the current controller that depends on a single object
304 * (we call this kind of sub templates for partials). It relies on the fact that partials should follow the naming convention of being
305 * prefixed with an underscore -- as to separate them from regular templates that could be rendered on their own.
307 * In a template for AdvertiserController::account:
309 * <?= $controller->render(array('partial' => 'account')); ?>
311 * This would render "advertiser/_account.tpl" and pass the instance variable $controller->account in as a local variable $account to
312 * the template for display.
314 * In another template for Advertiser::buy, we could have:
316 * <?= $controller->render(array('partial' =>'account','locals'=>array('account'=>$buyer))); ?>
318 * <?php foreach($advertisements as $ad) : ?>
319 * <?= $controller->render(array('partial'=>'ad','locals'=>array('ad'=>$ad))); ?>
320 * <?php endforeach; ?>
322 * This would first render "advertiser/_account.tpl" with $buyer passed in as the local variable $account, then render
323 * "advertiser/_ad.tpl" and pass the local variable $ad to the template for display.
325 * == Rendering a collection of partials
327 * The example of partial use describes a familiar pattern where a template needs to iterate over an array and render a sub
328 * template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders
329 * a partial by the same name as the elements contained within. So the three-lined example in "Using partials" can be rewritten
330 * with a single line:
332 * <?= $controller->render(array('partial'=>'ad','collection'=>(array)$advertisements)); ?>
334 * This will render "advertiser/_ad.tpl" and pass the local variable +ad+ to the template for display. An iteration counter
335 * will automatically be made available to the template with a name of the form +partial_name_counter+. In the case of the
336 * example above, the template would be fed +ad_counter+.
338 * == Rendering shared partials
340 * Two controllers can share a set of partials and render them like this:
342 * <?= $controller->render(array('partial'=>'advertiser/ad', 'locals' => array('ad' => $advertisement ))); ?>
344 * This will render the partial "advertiser/_ad.tpl" regardless of which controller this is being called from.
346 function renderPartial($partial_path, $object, $local_assigns = array())
348 $path = $this->_partialPathPiece($partial_path);
349 $partial_name = $this->_partialPathName($partial_path);
351 $object =& $this->_extractingObject($partial_name, $local_assigns);
352 $local_assigns = array_merge((array)@$this->_controllerInstance->_assigns, (array)$local_assigns);
353 $this->_addObjectToLocalAssigns_($partial_name, $local_assigns, $object);
354 return $this->renderFile((empty($path) ? '' : $path.DS).'_'.$partial_name, true, $local_assigns);
357 function renderPartialCollection($partial_name, $collection, $partial_spacer_template = null, $local_assigns = array())
359 Ak::profile('Rendering partial Collection'.$partial_name);
360 $collection_of_partials = array();
361 $counter_name = $this->_partialCounterName($partial_name);
362 if(empty($local_assigns[$counter_name])){
363 $local_assigns[$counter_name] = 1;
366 foreach ($collection as $counter=>$element){
367 $local_assigns[$counter_name] = $counter+1;
368 $collection_of_partials[] = $this->renderPartial($partial_name, $element, $local_assigns);
371 Ak::profile('Finished rendering partial Collection'.$partial_name);
373 if (empty($collection_of_partials)) {
374 return ' ';
377 if (!empty($partial_spacer_template)){
378 $spacer_path = $this->_partialPathPiece($partial_spacer_template);
379 $spacer_name = $this->_partialPathName($partial_spacer_template);
380 return join((empty($spacer_path) ? '' : $spacer_path.DS).'_'.$spacer_name,$collection_of_partials);
381 }else{
382 return join('',$collection_of_partials);
386 function renderCollectionOfPartials($partial_name, $collection, $partial_spacer_template = null, $local_assigns = array())
388 return $this->renderPartialCollection($partial_name, $collection, $partial_spacer_template, $local_assigns);
392 function _partialPathPiece($partial_path)
394 if(strstr($partial_path, '/')){
395 $dir_name = dirname($partial_path);
396 if(strstr($dir_name,'/')){
397 return $dir_name;
398 }else{
399 return AK_VIEWS_DIR.DS.$dir_name;
401 }else{
402 return '';
406 function _partialPathName($partial_path)
408 return strstr($partial_path, '/') ? basename($partial_path) : $partial_path;
411 function _partialCounterName($partial_name)
413 return array_pop(explode('/',$partial_name)).'_counter';
416 function &_extractingObject($partial_name, &$deprecated_local_assigns)
418 if(is_array($deprecated_local_assigns)){
419 return $this->controller->$partial_name;
420 }else{
421 return $deprecated_local_assigns;
425 function _addObjectToLocalAssigns($partial_name, $local_assigns, &$object)
427 $local_assigns[$partial_name] = empty($object) ? $this->controller->$partial_name : $object;
430 function _addObjectToLocalAssigns_($partial_name, &$local_assigns, $object)
432 if(!empty($object)){
433 $local_assigns[$partial_name] = $object;
434 }elseif(!empty($this->controller->$partial_name)){
435 $local_assigns[$partial_name] = $this->controller->$partial_name;
441 * Variables assigned using this method will act on any controller or action. Use this in conjunction
442 * with your application helpers in order to allow variable passing from inside your views.
443 * This is used for example on the capture helper.
445 * @static
447 function _addGlobalVar($var_name, $value, $_retrieve = false)
449 static $_global_vars = array();
450 if($_retrieve){
451 return $_global_vars;
453 if($var_name[0] != '_'){
454 $_global_vars[$var_name] =& $value;
458 * @static
460 function _getGlobals()
462 return AkActionView::_addGlobalVar(null,null,true);