1
\$\begingroup\$

I am building a PHP framework and would like to get some feedback on a few different section of the project so far. I consider myself still a neophyte in PHP so I would like to ask if I'm going about completing this different task in an efficient and or correct way.

This section is of the Template parser. I wish to create a parser that will take html files with only tags [@tag] and no php and have it parse the file replacing the tags with supplied data and return it if it need to be used in the parsing of another template.

From research and a personal idea I thought it would be best to try and divide the parsing mechanics and the injecting of dynamic data/info. So I created two classes the Page class and Template class. The template class over sees the parsing of the html files and the page over sees that tag replacement.

Template Class (instantiates the page class)
template.object.php

class Template{
    private $appath;
    private $page;
    protected $settings;
    private $issettings;

    public $tags = array();
    public $store = array();
    public $template = array();

    //Set path for templat use
    public $path;

    /**
     * Set app_path, include page class
     * Create Page instance
     */
    public function __construct(){
        $this->appath = APP_PATH;
        require_once(dirname(__FILE__).'/'.'page.object.php');
        $this->page = new Page();
    }
    /**
     * Set the path to template file
     * values are added to an array
     * @param Int $tempkey - Key value to be used to identify value
     * @return Int $tempkey used
     */
    public function SetTemplateFile($tempkey,$file){
        if(isset($this->settings)){
            $template = $this->appath.'/styles/'.$this->settings.'/templates/'.$file.'.template.html';
            if(file_exists($template)){
                $this->template[$tempkey] = $template;
                return $tempkey;
            }else{
                trigger_error('Temple file not found',E_USER_ERROR);
            }

        }

    }
    /**
     * Retrieves set template files
     * Note: Retrieved from an array Key identifier needed
     * @param String or Int $key - Index key used to 
     * @return Int $tempkey used
     */
    public function GetTemplateFile($key){
        if(array_key_exists($key,$this->template)){
            return $this->template[$key];
        }else{
            trigger_error('Template File key not found',E_USER_ERROR);
        }
    }
    /**
     * Manually set temple block tags content
     * Note: Manually add content to a section of the template
     * @param String $tags - the template tag to place $value
     * @param String $value - content to replace $tag
     * @return String of the tag selected
     */
    public function SetTags($tags,$value){
        $this->tags[$tags] = $value;
        return $tags;
    }
    /**
     * Set a group of tags with using an array
     * Note: meant to use a multidimensional array
     * @param Array $array - multidimensional array with the tag as the key and value as the tag value
     * @return Null
     */
    public function SetGroupTags($array){
        if(is_array($array)){
            foreach ($array as $subarray) {
                foreach ($subarray as $key => $value) {
                    $this->tags[$key] = $value;
                }
            }
        }else{
            trigger_error('Paramete for not an array '.gettype($array).' type given',E_USER_ERROR);
        }
    }
    /**
     * Set a group of tags with using an array
     * Note: Meant to work with a multidimensional array
     * Stores the data in member variable $store 
     * @param Array $array - multidimensional array with the tag as the key and value as the tag value
     * @return Null
     */
    public function SetBlock($tempkey,$array){
        if(file_exists($this->template[$tempkey])) {           
            foreach ($array as $subarray) {
                $output = file_get_contents($this->template[$tempkey]);
                foreach ($subarray as $key => $value) {
                    $tagToReplace = "[@$key]";
                    $output = str_replace($tagToReplace, $value, $output);
                }
                $this->store[] = $output;
            }
        }else{
            trigger_error('Unable to set template block',E_USER_ERROR);
        }
    }
    /**
     * Phrases values stored in member variable $store
     * @param Null
     * @return String phrased template block
     */
    public function RunBlock(){
        foreach ($this->store as $value) {
            $output .=$value."\n";
        }
        //Temp FIX
        unset($this->store);
        return $output;
    }
    /**
     * Set page template tags with meta, script, and style tags value
     * @param Null
     * @return Null
     */
    public function AddHeadFiles(){
        $this->tags['meta'] = $this->page->GetMeta();
        $this->tags['script'] = $this->page->GetJS();
        $this->tags['styles'] = $this->page->GetCSS();
    }
    /**
     * Set page template tags with meta, script, and style tags value
     * @param Null
     * @return Null
     */
    public function Output($key){
        if (file_exists($this->template[$key])) {
            //Add JavaScripts, StyleSheets and Meta tags info using (PageCon method)
            $this->AddHeadFiles();
                $output = file_get_contents($this->template[$key]);
                foreach ($this->tags as $key => $value) {
                    $tagToReplace = "[@$key]";
                    $output = str_replace($tagToReplace, $value, $output);
                }
                return $output;
            }else{
                trigger_error('Unable to phrase template block',E_USER_ERROR);
                return false;
            }

    }
    /**
     * Set the styles directory to use
     * Note: Settings is used to reference the directory to use
     * when working this a template files 
     * 
     * @param String $data - Name of directory of template files 
     * @return Null
     */
    public function SetSettings($data){
        if($this->IsSetting($data)){
            $this->settings = $data;
            //Add the [@path] tag with
            //To supply a direct path for the css and js files
            $this->tags['path'] = 'styles/'.$data;
            //Send setting to PageCon
            $this->page->GetTempSet($data);
        }else{
            trigger_error('Invalid parameter of: ('.$data.')',E_USER_ERROR);
        }

    }

    /**
     * Check to see if a directory with the name of the setting valus
     * 
     * @param String $data - setting valus being used
     * @return true or Null
     * 
     * If error is caught for the DirIterator throws and exception
     */
    public function IsSetting($data){
        $this->issettings = $this->appath.'/styles/'.$data;
        try{
            $dir = new DirectoryIterator($this->issettings);
            if($dir->isDir()){
                return true;
            }
        }catch(Exception $e){
            echo '<strong>Template Error: in Class Template{} Line '.$e->getLine().'</strong><br />';
        }

    }

    public function __destruct(){
        //Empty
    }



}

Page Class
page.object.php

class Page{
    //Header tags
    public $title;
    public $meta = array();
    public $styles = array();
    public $scripts = array();

    private $settings;

    //Body tage
    public $menu = array();
    public $bodyTags = array();
    public $addBodyTags;

    public function __construct(){
        //Empty
    }

    /**
     * Set the metatags to be added to the page
     * @param Array $array - an array of the meta tags and information
     * @return Null
     */ 
    public function SetMeta($array){
        foreach ($array as $value) {
            foreach ($value as $key => $value) {
                $output .= ''.$key.'="'.$value.'" ';
            }
            $this->meta[] = "<meta $output />";
            unset($output);
        }
    }
    /**
     * Retrieve the metatag information set
     * @param Null
     * @return string of metatag information
     */ 
    public function GetMeta(){
        foreach ($this->meta as $metatag) {
            $output .= $metatag."\n";
        }
        return $output;
    }
    /**
     * Set template setting for Template class
     * @param String $settings - template file to use when generating page
     * @return Null
     */ 
    public function GetTempSet($settings){
       $this->settings = $settings; 
    }
    /**
     * An array of JavaScript files
     * @param Array $array - js files to add
     * @return Null
     */
    public function AddJS($array){
        $arg = explode(',',$array);
        for ($i = 0; $i < count($arg); $i++){
            if(strpos($arg[$i],'.js')){
               $this->scripts[] = '<script src="styles/'.$this->settings.'/js/'.$arg[$i].'" type="text/javascript"></script>'; 
            }else{
               $this->scripts[] = '<script src="styles/'.$this->settings.'/js/'.$arg[$i].'.js" type="text/javascript"></script>'; 
            }

        }
    }
    /**
     * Retrieve a list of the added JavaScript files to include into the page
     * @param Null
     * @return String list of js files
     */
    public function GetJS(){
        foreach ($this->scripts as $script) {
            $output .= $script."\n";
        }
        return $output;
    }
    /**
     * An array of CSS files
     * @param Array $array - css files to add
     * @return Null
     */   
    public function AddCSS($array){
        $arg = explode(',',$array);
        for ($i = 0; $i < count($arg); $i++){
            if(strpos($arg[$i],'.css')){
               $this->styles[] = '<link rel="stylesheet" href="styles/'.$this->settings.'/css/'.$arg[$i].'"/>'; 
            }else{
               $this->styles[] = '<link rel="stylesheet" href="styles/'.$this->settings.'/css/'.$arg[$i].'.css"/>'; 
            }

        }
    }
    /**
     * Retrieve a list of the added CSS files to include into the page
     * @param Null
     * @return String list of css files
     */
    public function GetCSS(){
        foreach ($this->styles as $style) {
            $output .= $style."\n";
        }
        return $output;
    }

    public function __destruct(){
        //Empty
    }
}

Here is an example of them being used to generate a html list using ulist.template.html and lilist.template.html

HTML files being used in esample
ulist.template.html

<ul>
  [@list]
</ul>

lilist.template.html

<li><a href="[@link]">[@text]</a></li>

PHP code EX

//Sample array of data
$array = array(
    array('link' => 'http://website1.com','text' => 'Website One'),
    array('link' => 'http://website2.net','text' => 'Website Two'),
    array('link' => 'http://website3.org','text' => 'Website Three')
);

$template = new Template;

//Set style to use for template
$template->SetSettings('demo');
//Set ulist.template.html as a temp file
//to be used
$ultag = $template->SetTemplateFile('ulist','ulist');
//Set lilist.template.html as a temp file
//to be used
$litag = $template->SetTemplateFile('lilist','lilist');
//Data to be inputed into the temp file
//data used from $array
$template->SetBlock($litag,$array);
//Parse the lilist.template.html file
//with $array data and return parsed temp file
$generatedlilist = $template->RunBlock();
//Add the parsed lilist into ulist.template file
$template->SetTags('list',$generatedlilist);
//Echo generated html list
echo $template->Output($ultag);

It then outputs the html

<ul>
<li><a href="http://website1.com">Website One</a></li>
<li><a href="http://website2.net">Website Two</a></li>
<li><a href="http://website3.org">Website Three</a></li>
</ul>

There a few steps to parsing a template file I'm not sure if that is a bad thing. I find that it makes building a little easier.

Is this a good way to build this type of function/feature? I am open to any tip, tricks, suggestions, and advice.

\$\endgroup\$
  • \$\begingroup\$ Why reinvent the wheel? Googling "PHP template engine" yields smarty, twig, and rain.tpl. Unless this is strictly a learning exercise, I'd use an existing engine. \$\endgroup\$ – Dave Jarvis Aug 4 '13 at 20:19
  • \$\begingroup\$ I'm building this for learning exercise and having a little framework when working on projects wouldn't hurt. Do you think this could be coded better? \$\endgroup\$ – andrewnite Aug 4 '13 at 23:21
  • \$\begingroup\$ What if the link value contains [@text]? \$\endgroup\$ – icktoofay Aug 4 '13 at 23:21
  • \$\begingroup\$ I did not think of that then <a href="Website One">Website One</a> Are you suggestion I make sure tags can not be used as values to be inputted into the template in the parsing process? \$\endgroup\$ – andrewnite Aug 4 '13 at 23:41
  • \$\begingroup\$ @andrewnite: Instead of using HTML as input, use XML. Instead of hard-coding tags deep inside PHP, use XSLT to transform XML input into HTML documents. \$\endgroup\$ – Dave Jarvis Aug 5 '13 at 0:57
1
\$\begingroup\$

I would separate the components as follows:

  • Database Layer (PL/SQL)
  • View Layer (XSLT)
  • Controller Layer (PHP)

Database Layer

Abstract out the database so that you can make generic calls to a database. For example:

  /**
   * Call a database function and return the results. If there are
   * multiple columns to return, then the value for $params must contain
   * a comma; otherwise, without a comma, the value for $params is used
   * as the return column name. For example:
   *
   *- SELECT $params FROM $proc( ?, ? ); -- with comma
   *- SELECT $proc( ?, ? ) AS $params; -- without comma
   *- SELECT $proc( ?, ? ); -- empty
   *
   * @param $proc Name of the function or stored procedure to call.
   * @param $params Name of parameters to use as return columns.
   */
  public function call( $proc, $params = "" ) {
    $args = array();
    $count = 0;
    $placeholders = "";

    // Key is zero-based (e.g., $proc = 0, $params = 1).
    foreach( func_get_args() as $key => $parameter ) {
      // Skip the $proc and $params arguments to this method.
      if( $key < 2 ) continue;

      $count++;
      $placeholders = empty( $placeholders ) ? "?" : "$placeholders,?";
      array_push( $args, $parameter );
    }

    $sql = "";

    if( empty( $params ) ) {
      // If there are no parameters, then just make a call.
      $sql = "SELECT schema.$proc( $placeholders )";
    }
    else if( strpos( $params, "," ) !== false ) {
      // If there is a comma, select the column names.
      $sql = "SELECT $params FROM schema.$proc( $placeholders )";
    }
    else {
      // Otherwise, select the result into the given column name.
      $sql = "SELECT schema.$proc( $placeholders ) AS $params";
    }

    $db = $this->getDataStore();
    $statement = $db->prepare( $sql );

    for( $i = 1; $i <= $count; $i++ ) {
      //$this->log( "Bind " . $i . " to " . $args[$i - 1] );
      $statement->bindParam( $i, $args[$i - 1] );
    }

    try {
      $result = null;

      if( $statement->execute() === true ) {
        $result = $statement->fetchAll( PDO::FETCH_ASSOC );
        $this->decodeArray( $result );
      }
      else {
        // \todo Send an e-mail.
        $info = $statement->errorInfo();
        $this->log( "SQL failed: $sql" );
        $this->log( "Error: ". $info[2] );
      }
    }
    catch( PDOException $ex ) {
      // \todo Send an e-mail.
      $this->log( $ex->getMessage() );
    }

    return $result;
  }

Be sure to replace schema. with your own schema name, or pass it in. This allows you to write:

$db->call( "stored_function", ... );

Where ... represent the parameters and name of the return value. For the most part, the database functions should return an XML document.

The XML document is the contract between the database and the controller. This not only separates concerns, but it means that you can swap databases and implementation languages independently. If your project needs PostgreSQL over MySQL, so be it: no PHP code needs to change. If your project needs to use Go instead of PHP, so be it: no database code needs to change.

View Layer

Write a wrapper around PHP's XSLT processor. This can then be used to transform the XML document into any data format you require. This includes XHTML, CSV, or even LaTeX (to generate high-quality PDFs). Such a class might resemble:

Use XSLTProcessor;
Use DOMDocument;

/**
 * Transforms XML data (into HTML, LaTeX, and XML).
 */
class Xslt {
  /** The XML DOM to transform (mandatory). */
  private $xmlDom;

  /** The stylesheet DOM to use for the transformation (mandatory). */
  private $xslDom;

  /** The XSLT Engine to use for transforming XML documents. */
  private $xslt;

  /**
   * Instantiates a new XSLTProcessor class.
   *
   * \todo Enforce XML and XSL data as arguments and make corresponding
   * set methods private?
   */
  public function __construct() {
    $this->xslt = new XSLTProcessor();
  }

  /**
   * Performs the XSL transformation and then returns the results. The
   * XML document must have been set using the setXml method.
   *
   * @see setXml
   * @return The results from transformToXML on the DOM using the
   * internal XSLTProcessor instance.
   */
  public function transform() {
    $this->getXsltEngine()->importStyleSheet( $this->getXslDom() );
    return $this->getXsltEngine()->transformToXML( $this->getXmlDom() );
  }

  /**
   * Sets the XML DOM based on the given data. This calls the toXmlDom
   * method.
   *
   * @param $xmlData An XML document as a string.
   * @see toXmlDom
   * @see setXmlDom
   */
  public function setXml( $xmlData ) {
    $this->setXmlDom( $this->toXmlDom( $xmlData ) );
  }

  /**
   * Sets the XML DOM based on the given data. The data must have
   *
   * @param $xmlDom A DOM object.
   * @see setXml
   */
  public function setXmlDom( $xmlDom ) {
    $this->xmlDom = $xmlDom;
  }

  /**
   * Converts the given XML data into a DOM object. This does not update
   * the internal DOM object.
   *
   * @param $data An XML document as a string to convert to a DOM.
   */
  public function toXmlDom( $data ) {
    $dom = $this->createDom();
    $dom->formatOutput = $this->getFormatOutput();
    $dom->preserveWhiteSpace = $this->getPreserveWhiteSpace();
    $dom->loadXML( $data );

    return $dom;
  }

  /**
   * Sets the XSL DOM.
   */
  public function setStylesheet( $stylesheet ) {
    $xsl = $this->createDom();
    $xsl->load( $stylesheet );

    $this->xslDom = $xsl;
  }

  /**
   * Passes a parameter to the XSLT engine, in the default namespace.
   */
  public function setParameter( $k, $v ) {
    $this->getXsltEngine()->setParameter( "", $k, $v );
  }

  /**
   * Returns the XML DOM to use for transformation.
   */
  private function getXmlDom() {
    return $this->xmlDom;
  }

  /**
   * Returns the XSL DOM to use for transformation.
   */
  private function getXslDom() {
    return $this->xslDom;
  }

  /**
   * Returns false to indicate that the XML output should not be formatted
   * in a human-readable way (i.e., indented).
   */
  private function getFormatOutput() {
    return false;
  }

  /**
   * Returns false to indicate that white space should not be preserved in
   * the final document.
   */
  private function getPreserveWhiteSpace() {
    return false;
  }

  /**
   * Returns the XSLT engine to use for transformations.
   */
  private function getXsltEngine() {
    return $this->xslt;
  }

  /**
   * Helper method to create a DOM object (used for XML and XSL DOMs).
   */
  private function createDom() {
    global $DEFAULT_CHARACTER_ENCODING;
    return new DOMDocument( "1.0", $DEFAULT_CHARACTER_ENCODING );
  }
}

You would have to write an XSLT stylesheet to transform the XML document into your desired output.

The advantage here is that the lines for <link ... /> need be written only once. You can completely do away with any element duplication in your source code by using XSLT.

A big disadvantage is that XSLT is not simple.

Controller Layer

Use PHP to parse the requests from the user, determine what type of output you want (XHTML vs. CSV vs. LaTeX), set any necessary parameters, then call the XSLT engine to transform the XML document retrieved from the database into your final output. In code, this might resemble:

// Get the requested command by the user.
$command = $_POST[ "command" ];

// Determine the appropriate method call.
$method = $this->lookup( $command );

// Execute the user's command.
$this->{$method}();

// Render the result.
$this->render();

You might, for example, map a command such as "update.username" to an updateUserName method:

private function updateUserName() {
  $id = $this->getId();
  $userName = $_POST[ "username" ];

  $result = $this->call( "set_username", $userName, "x" );

  $this->setXml( $result[0]["x"] );
}

private function render() {
  $xslt = new Xslt();
  echo $xslt->transform();
}

Now you can:

  • Control the look of the application from XSL files.
  • Vary the database and the implementation language independently.
  • Avoid proliferating CRUD operations throughout the PHP code base.
  • Limit the amount of duplicated code throughout the entire application.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.