Subversion Repositories phpLibraryV2

Rev

Rev 10 | Blame | Compare with Previous | Last modification | View Log | Download | RSS feed

<?php

/*
   Very basic, but uncomplicated, templating system for PHP.
   
   Template class takes a template and merges it with data. Templates can be any 
   format; PostScript, text, HTML, whatever. The bottom line is, Template will look for
   special tag in the file and replace them with values passed to it in a has (called $data here).

   The following template illustrates all current tags:

   <table>
         <loop owners>
            <tr>
               <td>
                 <ifdatafield selected><b></ifdatafield>
                  <datafield name>
                 <ifdatafield selected></b></ifdatafield>
               </td>
               <td><datafield address></td>
            </tr>
         </loop>
   </table>
   <p>This is version <datafield Version> of the file</p>

   The special tags above are <loop>, <ifdatafiled>, <datafield> (the first two with their
   closing tags indicated by a / as in HTML).

   The following PHP code snippet illustrates a data file that could be passed to the above:

      $data = array( 
                     'owners' => array(
                         array( 'name' => 'Rod', 'address' => 'Dallas' ),
                         array( 'name' => 'Nancy', 'address' => 'Dallas' ),
                         array( 'name' => 'Chick', 'address' => 'Davao', 'selected' => true ),
                         array( 'name' => 'Rhonda', 'address' => 'Houston' )
                      ),
                      'Version' => '2.0.5',
                      'unused' = 'some arbitrary thing'
                   );
                   
   Note that in the above template, a table is defined, then the special tag <loop owners> is
   introduced. This will process everything up to the </loop> using the data found in 
   $data['owners']. For each loop, if the key 'selected' is defined a 'bold' (<b> .. </b>) is
   placed around the first data field. After that, the element whose key is 'name' and 
   'address' are inserted. After the loop, the rest of the hash is processed, with the value of
   $data['Version'] being used in the last paragrap. Note that $data['unused'] is not used in
   the template.
*/


class Template {

   protected static $regexes = array( // note, using # as delimiter so don't have to escape /
                    'loop' => '#<loop +([^>]+)>(.*?)</loop>#si',
                    'iffield' => '#<ifdatafield +([^>]+)>(.*?)</ifdatafield>#si',
                    'varname' => '#<datafield +([^>]+)>#si',
                    'session' => '#<session +([^>]+)>#si'
                 );
   protected static $delimiters = array( 'open' => '<', 'close' => '>' );
   protected $filename; // holds the name of the template file
   
   // this is a key inside the data hash that holds the value. If empty, the value is treated as the value
   // for example, if $hashKey is set to 'value', data used in processTemplate is assumed to be $data[$key]['value']
   // instead of just $data[$key]
   protected $hashKey = '';

   public static $templatePath; // path to search for templates
   public $template; // holds entire template for processing
   
   /*
    * name:        constructor
    * parameters:  $filename -- optional file name for template
    *              $templatePath -- optional template path (full path on disk)
    * returns:     nothing
    * description: If filename is given, will prepend $templatePath to it and attempt to load
    *              from disk.
    *              if $templatePath is defined, will set the path FOR ALL INSTANCES. 
    */

   public function __construct ( $filename = null, $templatePath = null, $hashKey = '' ) {
      if ( isset( $templatePath ) ) {
         self::$templatePath = $templatePath;
      } // if
      if ( isset( $filename ) ) {
         $this->FileName( $filename );
         $this->loadTemplate();
      } // if
      $this->hashKey = $hashKey;
   } // construct
   
   /*
    * name:        FileName
    * parameters:  $filename -- optional override of filename stored in instance
    * returns:     $this->filename prior to (optional) replacing it with $filename param
    * description: Helper function which ensures path prepended to file name, then stores it
    */

   public function FileName ( $filename = null ) {
      $save = $this->filename;
      if ( isset ( $filename ) ) {
         $this->filename = self::$templatePath . $filename;
      }
      return $save;
   } // setFileName
   
   /*
    * name:       Template
    * parameters: $template -- a template to be stored
    * retuns:     The template before it was (optionally) replaced
    * description: Helper function to set/get $this->template. Called with no parameters, will
    *             simply return the contents for the current template. Called with a paramter
    *             will set the template to that value (and return the old template)
    */
   
   public function Template( $template = null ) {
      $save = $this->template;
      if ( isset( $template ) ) {
         $this->template = $template;
      }
      return $save;
   }

   /*
    * name:        loadTemplate
    * parameters:  $filename -- optional override of filename stored in instance
    * returns:     nothing
    * description: Loads the template file from disk
    */

   public function loadTemplate ( $filename = null ) {
      if ( isset( $filename ) ) {
         $this->FileName( $filename );
      }
      if ( is_readable( $this->filename ) ) {
         $this->template = file_get_contents( $this->filename );
      } else {
         throw new Exception( "File [$this->filename] does not exist or is unreadable", 15 );
      }
   } // loadTemplate
   
   /*
    * name:        process
    * parameters:  $dataset   -- array of hash containing values to replace
    * returns:     the processed template
    * description: Simply ensures the template is loaded, then calls processTemplate with a 
    *              copy. we will work on a copy of the template. Once loaded, the template can
    *              only be modified by another load. Thus, we could use the same instantiation
    *              with multiple calls to process using differnt data sets.
    */

   public function process ( $dataset ) {
      if ( ! isset( $this->template ) ) {
         $this->loadTemplate();
      } // if
      $template = $this->template;
      return $this->processTemplate( $dataset, $template );
   } // process
   
   /*
    * name:        processLoop
    * parameters:  $template -- The contents to be processed (exclusive of the <loop> construct)
    *              $data     -- array of hash containing values to replace
    * returns:     A copy of $template for each row, with all data fields filled in
    * description: For each row in $data, a copy of $template is processed by passing it back to
    *              processTemplate.
    * NOTE: This is a recursive function. It is called by processTemplate, then calls
    *       processTemplate. However, since (currently) loops can not be nested, the memory
    *       usage should be minimal
    */

   function processLoop ( $data, $template ) {
      // since we will repeat this over and over, make a copy of the template segment
      $original_code = $template;
      $returnValue = ''; // we will add to this for each loop
      
      foreach ( $data as $value ) { 
         $template = $original_code;
         $returnValue .= $this->processTemplate( $value, $template );
      } // foreach
      if ( ! isset( $returnValue ) ) {
            $returnValue = '<p>No Values Found</p>';
      }
      return $returnValue;
   }
    

   /*
    * name:        processTemplate
    * parameters:  $template -- The filename containing the template to be opened and processed
    *              $data     -- an array of hashes to populate the template with
    * returns:     A copy of the contents of the template with all fields filled in
    * description: First, all tag pair of the form <loop var=VARNAME> . . . </loop> are looked for
    *              and processed. The information between <loop>...</loop> and the hash entry
    *              VARNAME are passed to processLoop. See that code for information. Upon return,
    *              then entire block is replaced with the results of processLoop
    *              NOTE: Nested loops not currently supported
    *
    *             Next, all tag pairs of the form <ifdatafield fieldname> . . . </ifdatafield> are 
    *             searched and, if fieldname is not an element of the hash, (is null or blank), 
    *             the inner area is removed. Otherwise, the surrounding tags are removed and the 
    *             content remains.
    *             
    *             The entire template is now searched for tags of the form <datafield fieldname>, 
    *             and this tag is replaced by the contents of the field fieldname from $data
    *             
    *             Finally, the entire template is once more searched for tags of the form 
    *             <session varname>, and these tags are replaced with the session (cgi) parameters.
    * notes:      A lot of people don't like the conditional expression, but it makes the code
    *             better here. So, note the preg_replace statements use a conditional based on 
    *             whether the value is set and, if it is not, does the replace with null
    * 
    *             Also, when we do the preg_match, we grab the entire block that was matched
    *             ($matches[0]) then, after we find the correct value, we simply call preg_replace
    *             with that as the value that needs to be replaced in the string
    * 
    *             Finally, since the regexes are actually defined above, it almost seems as if
    *             we could have this whole function one big loop to go through each regex in turn
    *             however, they should be processed in the correct order
    *                loops are processed first. If not, they would be overwritten by the later ones
    *                conditionals may remove entire blocks, so they are processed next
    *                variable substitution is processed next
    *                session variables are dead last; generally used for screen wide things
    *             
    *             The processed template is returned by the routine.
    */
                 
   private function processTemplate ( $data, $template ) {

      // if they used a hashKey (the value is stored in an element of $data), replace the hash with the value
      // so, $data[$key][$this->hashkey] becomes just $data[$key]
      //print '<pre>processTemplate::data' . print_r( $data, true ) . '</pre>';
      if ( $this->hashKey ) {
         $tempData = array();
         //print "<pre>Processing hashKey $this->hashKey\n</pre>";
         foreach ( $data as $key => $value ) {
            //print "<pre>Checking $key\n</pre>";
            if ( is_array ($value ) && isset( $value[$this->hashKey] ) ) {
               //print "<pre>   it matches\n</pre>";
               $tempData[$key] = $value[$this->hashKey];
            } else {
               $tempData[$key] = $value;
            }
         }
         $data = $tempData;
      }
      //print "<pre>processTemplate::data\n" . print_r( $data, true ) . '</pre>'; 
      //print "Template is <pre>$template</pre>"; 
      //die;
      // process loops first
      while ( preg_match( self::$regexes['loop'], $template, $matches ) ) {
         $block = preg_quote($matches[0], '/'); // the full block that matches, so we can replace it
         $dataKey = $matches[1]; // this is the name of the key into $data
         $segment  = $matches[2]; // and this is the template segment to be processed
         //return '<pre>' . print_r($dataKey, true) . print_r( $segment, true ) . '</pre>';      
         # do the actual replacement on the template
         $template = preg_replace( "/$block/", 
                                   isset( $data[$dataKey] ) ? 
                                          $this->processLoop( $data[$dataKey], $segment ) :
                                          '', 
                                   $template );
      }
      // then, conditionals
      while ( preg_match( self::$regexes['iffield'], $template, $matches ) ) {
         $block = preg_quote($matches[0], '/'); // the full block that matches, so we can replace it
         $field = $matches[1]; // the name of the field we are looking for
         $string = $matches[2]; // what to replace it with
         // process and replace
         $template = preg_replace( "/$block/", 
                     isset( $data[$field] ) ? $string : '', 
                     $template );
      } // while
      // next, any direct variable replacements
      while ( preg_match( self::$regexes['varname'], $template, $matches ) ) {
         $block = preg_quote($matches[0], '/'); // the full block that matches, so we can replace it
         $varname = $matches[1]; // the variable whose value we want here
         //print "<pre>$varname\n" . print_r($data[$varname], true) . "</pre>";
         // replace
         
         $template = preg_replace( "/$block/", 
                                 isset( $data[$varname] ) ? $data[$varname] : '',
                                 $template );
         
      } // while
      // and finally, session variables
      while ( preg_match( self::$regexes['session'], $template, $matches ) ) {
         $block = preg_quote($matches[0], '/'); // the full block that matches, so we can replace it
         $varname = $matches[1]; // the session variable name
         // replace
         $template = preg_replace( "/$block/", 
                                   isset( $_SESSION[$varname] ) ? $_SESSION[$varname] : '',
                                   $template) ;
      } // while
      return $template;
   }
   

} // classs Template

?>