Templated views

Hi

I know the reason for going away from templates is a speed issue, but consider this: If we "precompile" the template to php then from that point forward we use the "compiled" version of the php file we would be just as efficient (well the first ht would be slower but after that it would be fast)

I tried this by adding the following methods to CBaseController



	public function getViewFileInternal($viewName, $viewFile)


	{


    if (constant('YII_DEBUG')===true) {


      // check to see if the file exists as a template


      if (is_file($viewFile.'.tpl')) {


        $fileOld = filemtime($viewFile.'.tpl');


        $fileNew = 0;


        if (is_file($viewFile.'.php')) {


          $fileNew = filemtime($viewFile.'.php');


        }


        if ($fileNew<$fileOld) {


          $this->generateFile($viewFile);


        }


      }


    }





    $viewFile .= '.php';


		return is_file($viewFile) ? Yii::app()->findLocalizedFile($viewFile) : false;


	}





	const REGEX_RULES='/<(/?)com:(([w]+)(.[w]+)?)((?:s*[w.]+s*=s*'.*?'|s*[w.]+s*=s*".*?"|s*[w.]+s*=s*<%.*?%>)*)s*(/?)>/msS';


	


	const REGEX_ATTRIBUTE_RULES='/(s*([w.]+)s*=s*"(.*?)")?/msS';





	protected function generateFile($viewFile) {


    // Open a stream for creating the file into


    $template = file_get_contents($viewFile.".tpl");


    // $xml =  simplexml_load_string("<d>".$template."</d>");


    $matches = array();


		$n=preg_match_all(self::REGEX_RULES,$template,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE);


		


		$lastMatchStart = 0;


		$newTemplate='';


		foreach($matches as $match) {


  		$matchStart=$match[0][1];





  		$closure    = $match[1][0];


  		$className  = $match[3][0];


  		$methodName = $match[4][0];


  		$attributes = $match[5][0];


  		$endclosure = $match[6][0];


  		$attributeMap = false;


  		if ($attributes) {


        // Map out the attributes


    		$na=preg_match_all(self::REGEX_ATTRIBUTE_RULES,$attributes,$attributes,PREG_SET_ORDER|PREG_OFFSET_CAPTURE);


    		$attributeMap = array();


    		foreach($attributes as $attribute) {


          $attributeMap[strtolower($attribute[2][0])] = $attribute[3][0];


    		}





  		}


  		$call = '';


  		if ($closure!=="/") {


        // Must be a static call


    		if ($methodName) {


          $methodName = substr($methodName,1);


          $reflection = new ReflectionMethod ($className,$methodName);


          $params = $reflection->getParameters();


          $call = "echo $className::$methodName( ";


          $ignoring = false;


          foreach($params as $param) {


            $name = strtolower($param->getName());


            if (isset($attributeMap[$name])) {


                if ($ignoring!==false) {


                  echo "Populated  '$name' after optional '$ignoring' ";


                  die();


                }


                $call .=$attributeMap[$name] . ",";


            }


            else if ($param->isDefaultValueAvailable()) {


                $ignoring = $name;


                // $call .=$param->getDefaultValue() . ",";


            }


            else {


              echo "Missing required paramater $name ";


              die();


            }


          }


          $call = substr($call,0,-1).")";


    		}


    		


    		$call = "<?" . "php $call ?".">";


  		}


  		if ($endclosure==="/" || $closure==="/") {


        switch ($className) {


          case "CHtml" :


            $methodName = substr($methodName,1);


            switch ($methodName) {


              case "form" : $call .= "</form>";


            default:


            	


            	break;


            }


        default:


        	


        	break;


        }


  		}


      $newTemplate .= substr($template, $lastMatchStart, $matchStart-$lastMatchStart) .


                     $call;





  		$lastMatchStart=$matchStart+strlen($match[0][0]);





		}


    $newTemplate .= substr($template, $lastMatchStart);





    $file = fopen($viewFile.".php", "w");


    fwrite($file,$newTemplate);


    fflush($file);


    fclose($file);


	}





Then I modified CWidget like (of course CController would also need to be modified)



	public function getViewFile($viewName)


	{


		return $this->getViewFileInternal($viewName, $this->getViewPath().DIRECTORY_SEPARATOR.$viewName);


	}





The test case was the mainMenu view I renamed it to mainMenu.tpl and put in the following code



<ul>


<?php foreach($items as $item): ?>


<li>


<com:CHtml.link Body="$item['label']" Url="$item['url']" htmlOptions="$item['active'] ? array('class'=>'active') : array()"/>


</li>


<?php endforeach; ?>


</ul>


And then when I loaded the main page the application created a new file (in the same location as the template folder) with the following in it…



<ul>


<?php foreach($items as $item): ?>


<li>


<?php echo CHtml::link( $item['label'],$item['url'],$item['active'] ? array('class'=>'active') : array()) ?>


</li>


<?php endforeach; ?>


</ul>


Of course this should be extended to allow for any widget to be invoked through the "<com:" template but for now as a proof of concept it seems to work with CHtml objects.

The Pros

  1. Most editors can now parse the document as XML (especially with "CHtml::form()")

  2. Allows for editors to perform an auto complete operation on tags.

  3. It really does not detract from performance except during that "first load"

  4. Its more legible the using php directly to invoke widgets

The cons:

  1. It does not save any typing

  2. May be confusing to see a compiled version along with the template in the same folder

Thoughts ?

NZ

This is really a nice attempt!

I would like to add a few more cons about using a template though:

  1. using a template means educating users a new language, which contradicts the goal Yii tries to reach. Of course, this may not be an issue for existing prado users.

  2. When there is an error in the template, using compiled version make debugging difficult because PHP would complain the error in the compiled version instead of the original template file.

I agree with you that performance is not a big issue here. Maybe I should change those render methods a bit to prepare for such kind of extension.

I have a lot of experience with eZ publish, which uses a compiled template language. I hve a couple of cons based on that.

Cons:

  1. Generating the compiled templates takes a while. It is not an issue on a production site as it is done only once, but annoying while developing becuse the templates are changed a lot.

  2. eZ publish don't discover changes in some templates so you have to clear the template cache manually. Annoying.

  3. In eZ publish we have to write fetch functions or template operators if we need to do something the template language cannot do. This is time consuming and adds overhead. Yii is very liberating as I can just add some php code in the view if I really need to.

8) I believe development time is better spent elsewhere.

rsubsonic: If you look at the implementation i suggested it does not eliminate the possibility of having raw php code in the template it only reduces it. Also the implementation only regenerates the file based on last update date of the files so recompile would only occur during on the changed template.

qiang: Thanks, by using a template system you reduce the "signal to noise" ratio (ie what the view is actual doing compared to the amount of code used to generate a view ). By doing so your code is clearer and you should see an increase in productivity rather then increasing the learning curve. I am intrigued on how would this be implemented as an extension, could you elaborate ?

Pro

  1. Suppression (Like prados Visibility flag) could be added as an attribute, resulting in a generated code block like <?php ? if (!$this->ShouldSuppress) echo CHtml::… ?>  like Suppress="$this->ShouldSuppress"

  2. I never remember the order of arguments for every method call, using attributes eliminates that problem.

NZ

Yes, maybe I should read before I post. I had another look at your implementation, and it would make using template files optional, right? I can definitely live with that.

I understand that your implementation is a proof-of-concept, but would it be better to store the compiled templates somewhere in WebRoot/protected/runtime so the webserver don't need to have write access to WebRoot/protected/views? It would also make it less confusing than having compiled .php,  manually created .php and .tpl files in the same directory.

The idea is to change CBaseController::renderInternal() because this is the ultimate method invoked by all render methods in controllers and widgets.

In this method, we check if there is a global adapter. If so, we let the adapter to do the actual rendering work. Otherwise, it falls back to the default implementation.

Ok, I just implemented this feature.

To use a customized template, implement and registere a viewRenderer application component which should implement the IViewRenderer interface.

In CBaseController::renderFile(), when it sees a viewRenderer is available, it will call the renderer's renderFile() instead.

rsubsonic:That's correct, I am only looking to add to the existing functionality not take away. I definitely agree with having the "complied" version live outside the actual application folder, perhaps in the 'final' version all view's "complied" (and "normal" versions) could be copied to a runtime area so in production there would just be one directory to check for both files (although with themes that may be a little more tricky to implement).

qiang: wow, by the time I finished typing this message you finished it already. Will this work for CWidgets && CControllers ? They call getViewFile before calling the renderFile method so that could fail…

NZ

This works for both widgets and controllers.

Instead of working with getViewFile, we deal with renderViewFile. The possible view renderer could be like the following:

  1. check if a compiled version exists for the view file to be rendered

  2. if not, compile it and save it to runtime directory using some unique name

  3. call $context->renderViewFileInternal() with the generated PHP view file and the parameters, where $context refers to the controller or widget instance. The reason we use $context to do the rendering is because we want to make $this in the view refer to the controller or widget.

Excellent, thanks for clarifying and implementing so quickly !!

And of course K++

NZ

Thank you.

Now the tricky part is how to define the template syntax.

I think for now we will not implement this in the Yii framework because we want to keep Yii relatively simple. Of course, if a user comes up with some nice syntax and renderer implementation, we will consider including it the formal release in future.

I'd really love to see this in Yii, especially as this would really help PRADO users to migrate big PRADO projects with many templates to Yii (i run one of those). What i also really like about the PRADO style templates is the fact that i can edit them with a XML Editor (which is not possible with the current Yii templates as they are not valid XML).

Greetings from Hamburg / Germany

  • rojaro -

Prado's template is also not valid XML. Also, the only thing that will benefit from the Prado syntax in Yii is when you are using widgets in a template. However, in Yii you also use a lot of helper functions as well as some PHP constructs (such as if, foreach).

Quote

Ok, I just implemented this feature.

To use a customized template, implement and registere a viewRenderer application component which should implement the IViewRenderer interface.

In CBaseController::renderFile(), when it sees a viewRenderer is available, it will call the renderer's renderFile() instead.

It would be really nice to have a “preset”, I mean, a viewRenderer which implements compilation for Prado style templates, integrated into Yii  ;)

Quote

Prado's template is also not valid XML. Also, the only thing that will benefit from the Prado syntax in Yii is when you are using widgets in a template. However, in Yii you also use a lot of helper functions as well as some PHP constructs (such as if, foreach).

Humm… that's exactly why I choose Prado over doing something with a Smarty-based framework, for example. I really don't like placing loops and conditionals in a view, and XML widgets seem more "components" than PHP code (LOL). Call me fool, but I feel this is placing logic into the view, even if it maybe is "view logic" and not "business logic" (I know this is debatable, there are tons of discussions related to Smarty and this kind of inclusion of code in views)

On the other hand, I don't know how complex a parser could be…

The parser is not too difficult to implement and be plugged into the yii framework. Yes, maybe we will provide some parsers for Prado or other template syntax. But at this moment, this is not the most imperative task.

I have started such a parser which I will make freely available after testing with the 1.0 beta release. The reason for the delay is that it is dependent on code existing only in the repository at the moment.

NZ

That sounds great. So what is the syntax that can be used?

I am looking at applying a subset of the template code supported by Prado. Using the "<com:" approach to widgets and static method class calls along with a limited "<%" support. The output will be able to combine multiple consecutive php directives into one statement as well as passing the "tag body" as a parameter to a widget or static method call.



<com:CHtml:link Url="$item['url']" htmlOptions="$item['active'] ? array('class'=>'active') : array()">


<%=$item['label']%>


</com:CHtml:link>


outputs ~



<?php echo CHtml::link("n".$item['label']."n" ,$item['url'],$item['active'] ? array('class'=>'active') : array()); ?>


A big difference from Prado is that attributes are always treated as 'raw' php code so if you want to pass a string you need to single quote it

I was also considering adding a white-space removal tool, do you know of any for php ?

nz

That looks good. I suggest some slight changes.

  1. Change the property value syntax: use name="{value}" to indicate that “value” is a PHP exp​ression or non-string typed data, and use name=“value” to indicate that “value” is a string value.

  2. Change <com:CHtml::link> to <helper:CHtml.link>. Reserve <com:> for widget use only.

  3. Translate <%= expr %> to <?php echo expre; ?>

I think you should not remove white spaces, because when the template has some syntax error, it will be easier for developers to find out where is the problem.