Goedenavond allen,

Op 2 december j.l. heb ik alhier een template parser class geplaatst. Hierop heb ik van Jelmer feedback gekregen, welke ik wil verwerken, maar kom er niet uit.

Hergebruik instellingen
Ik wil graag ondervangen dat instellingen nogmaals gebruikt kunnen worden bij andere instellingen i.v.m. veiligheidsproblemen.

Voorbeeld:

<?php
$data = array(
   array(
      "username" => "Jan {password}",
      "password" => "henk24"
   )
);

// Uitkomst:
// Jan henk24
?> 


Hoe kan ik dit ondervangen??

Onbeperkt diepe array's
De instellingen (settings) en loops zijn respectievelijk maximaal 3 en 2 niveaus diep. Door middel van o.a. preg_replace_callback zou dit te ondervangen zijn. Hierdoor moet het dan ook mogelijk worden om loop in loop te maken.

Voorbeeld:

<?php //kleurtjes
<body>
   <div class="menu">

      <!-- loop menu start -->
         <div class="menu">

            <!-- loop item start -->
               <div class="item"></div>
            <!-- loop item start -->

         </div>
      <!-- loop menu end -->

   </div>
</body>
?>


Maar hoe?
Vraag 1 zou je kunnen oplossen door alle variabelen in één keer te vervangen. Bijvoobeeld, heel gemakkelijk, met [php]preg_replace_callback[/php]. Daarmee kan je ook ingewikkeldere patterns maken, om bijvoorbeeld tags a la {username|escape} (variabele + filter) te gebruiken:
<?php

$values = array('alfa' => '{beta}', 'beta' => 'gamma');

function find_replacement($matches)
{
global $values;

return $values[$matches[1]];
}

$template = 'Hey {alfa} {beta} gamma';

echo preg_replace_callback('{\{(\w+?)\}}', 'find_replacement', $template);
// geeft Hey {beta} gamma gamma
?>
De eerste vraag heeft Jelmer net al beantwoord, ga ik niet verder op in.

Je vraag 2, die snap ik echter niet. Wat bedoel je nou precies? met is_array() kan je kijken of iets een array is, en dan een recursieve functie maken? of bedoel je dat niet .. ?
@Jelmer
Wat nou in geval van een class? Onderstaande werkt n.l. niet... :s :s :s

<?php
class sTemplate
{
   public $dir = "./templates/"; // Templates dir
   public $ext = ".html"; // Template files extension
   public $atr = "class"; // HTML attribute, which value is name of variable

   public function parse ($template, $values)
   {
      $path = $this->dir . $template . $this->ext;
      $content = implode("", file($path));
      $result = preg_replace_callback('{\{(\w+?)\}}', 'replacement', $content);

      return $result;
   }

   private function replacement ($matches)
   {
      global $values;

      return $values[$matches[1]];
   }
}
?>


@niek
Jawel, maar zo'n recursieve functie vat ik niet echt
<?php

$result = preg_replace_callback(..., array($this, 'replacement'), ...);

?>

Ik heb een voorbeeldje gemaakt van hoe je loops zou kunnen afhandelen. Hopelijk is het een goed voorbeeld: Template.php

<?php

error_reporting(E_ALL);

class Template
{
private $template;

private $data_stack;

public function __construct($template)
{
$this->template = $template;

// SplStack is PHP 5.3.
// Voor PHP < 5.3, gebruik http://phphulp.ikhoefgeen.nl/SplStack.phps
$this->data_stack = new SplStack();
}

public function apply($data)
{
return $this->parse($this->template, $data);
}

private function parse($template, $data)
{
// push data op de stack, zodat de replace-functies weten in welke data ze moeten zoeken voor het vervangen van variabelen.
$this->data_stack->push($data);

// parse alle blokken, recursief. Ieder blok komt weer door deze zelfde method heen.
$result = preg_replace_callback('{\{(\w+?)\}(.+?)\{/\\1\}}', array($this, 'parse_block'), $template);

// Werk van binnen naar buiten. Handel eerst de blokken af (hierboven) en dan pas de variabelen
$result = preg_replace_callback('{\{(\w+)\}(?!.*\{/\\1\})}', array($this, 'parse_placeholder'), $result);

// haal de verwijzing naar de data weer weg, zodat we in het omliggende blok weer bij de goeie data komen.
$this->data_stack->pop();

return $result;
}

private function parse_block($matches)
{
list(, $block_name, $block_code) = $matches;

$data = $this->data_stack->top();

$block_data = $data[$block_name];

$results = array();

foreach($block_data as $step_data)
{
$results[] = $this->parse($block_code, $step_data);
}

return implode($results);
}

private function parse_placeholder($matches)
{
list(, $placeholder_name) = $matches;

$data = $this->data_stack->top();

return $data[$placeholder_name];
}

}

// Dit is de meest onbegrijpelijke test-data ooit. Trust me.

$template = 'Hoi {block1} (begin-block1) {var_y} {block2} (begin-block2) {var_x} (eind-block2) {/block2} test2 (eind-block1) {/block1} Hoi2';

$data = array(
'block1' => array(
0 => array(
'var_y' => 'XXvar_y1XX',
'block2' => array(
0 => array('var_x' => 'XXvar_x11'),
1 => array('var_x' => 'XXvar_x12'),
2 => array('var_x' => 'XXvar_x13'),
)
),
1 => array(
'var_y' => 'XXvar_y2XX',
'block2' => array(
0 => array('var_x' => 'XXvar_x21'),
1 => array('var_x' => 'XXvar_x22'),
2 => array('var_x' => 'XXvar_x23'),
)
)
)
);

$x = new Template($template);

echo $x->apply($data);

?>

Kort samengevat, ik loop de code af op zoek naar {block} {/block}, en zodra ik dat vind, beschouw ik dat block als een nieuw template, met een eigen setje variabelen, en ga ik dat op dezelfde manier parsen. (Dus eerst weer de blokken daarin, en dan de variabelen daarin).

De stack is voor de callback-functies om bij te houden welke data ze moeten gebruiken. In andere programmeertalen kan je dat wel met closures doen (PHP 5.3 bijvoorbeeld ;) ) maar voor PHP moet het maar even zo. De stack is zeg maar een tijdelijke verwijzing naar $data, maar hij moet ook de voorgaande waarden van $data onthouden omdat de functie zichzelf binnen zichzelf aanroept.

(Die class maakt gebruik van een class genaamd SplStack, een class die alleen in PHP 5.3 zit. Gebruik je die niet, dan moet je even SplStack.php includen.)

edit: code even hier geplakt.
Hmm, die code van gisteravond heeft hetzelfde probleem als jou vorige versie. Als ik een van die var_x waarden in $data aanpas naar '{var_y}', wordt hij vervangen.

Deze heeft daar geen last van Template_r2.phps. Die vervangt de blokken niet direct, maar vervangt ze door variabelen, waardoor ze tegelijkertijd met de andere variabelen pas in $result komen. En de variabelen binnen de waarden van de variabelen worden niet vervangen. Probleem opgelost. (En hij maakt geen gebruik meer van SplStack, omdat die niet werkt met references, wat helaas wel nodig is omdat ik geen closure heb gebruikt)
Jelmer, bedankt voor je geweldig uitgebreide reactie! Afgelopen dagen even met wat andere zaken bezig geweest. Jouw script brengt het idee een stuk dichter in de buurt. Nadeel daarvan wel, dat de match met HTML weg is.

Heb de regex voor de block's aangepast, nu heb je een duidelijkere scheiding tussen de variabelen en block's. Dit is alleen een nadeel zodra je de array voor instellingen wilt gebruiken.

<?php
//Ge-edit
?>

Het idee met die instellingen en loops etc.:

<?php
$data = array(
   'block1' => array(
      0 => array(
         'var_y' => 'XXvar_y1XX',
         'block2' => array(
            0 => array('var_x' => 'XXvar_x11'),
            1 => array('var_x' => 'XXvar_x12'),
            2 => array('var_x' => 'XXvar_x13'),
         )
      ),
      1 => array(
         'var_y' => 'XXvar_y2XX',
         'block2' => array(
            0 => array('var_x' => 'XXvar_x21'),
            1 => array('var_x' => '{var_y}'), // Deze verwijst terug naar 'var-y' hierboven. Zou niet moeten!
            2 => array('var_x' => 'XXvar_x23'),
         )
      )
   ),
   'block3' => 'XXvar_uXX'
);

// Drie mogelijkheden om bovenstaande array's in de template te verwerken:
$optie_1 = 'Hoi <!-- loop block1 start --> <div class="block1"> <!-- loop block2 start --> <div class="block2"></div> <!--/block2--> </div> <!-- loop block1 end-->';

$optie_2 = 'Hoi <!-- loop block1 start --> <div class="block1"> {var_y) is tegenovergesteld aan {block3}, want {block2:1:var_x} mag niet! </div> <!-- loop block1 end-->';

$optie_3 = '<meta name="version" content="{block2:0:var_x}" />';
?>
Het is nu mogelijk om een template-file te laden, óf om een losse string in te voeren. Tevens worden de HTML elementen nu geparst.

ToDo:
1. <DIV> in <DIV> werkt nog niet helemaal zoals het hoort
2. Waarden in placeholder kunnen laten combineren m.b.v. scheidingsteken ':'

<?php
error_reporting(E_ALL);

class TemplateStack
{
private $data = array();

public function push(&$data)
{
return $this->data[] =& $data;
}

public function &pop()
{
$reference =& $this->top();

// TemplateStack::top() verplaatst de cursor al naar het einde, dus key() zal de juiste key pakken.
unset($this->data[key($this->data)]);

return $reference;
}

public function &top()
{
// Cursor naar einde bewegen, zodat key() de index van het laatste element pakt.
end($this->data);

return $this->data[key($this->data)];
}
}

class Template
{
private $template;
private $data_stack;

public $atr = 'class';
public $dir = './';
public $ext = '.tpl';

public function __construct($template, $file = true)
{
if ($file == true)
{
$path = $this->dir . $template . $this->ext;
$this->template = file(implode("", $path));
}
else
{
$this->template = $template;
}

$this->data_stack = new TemplateStack();
}

public function apply($data)
{
return $this->parse($this->template, $data);
}

private function parse($template, $data)
{
// Push data op de stack, zodat de replace-functies weten in welke data ze moeten zoeken voor het vervangen van variabelen.
$this->data_stack->push($data);

// Parse alle blokken, recursief. Ieder blok komt weer door deze zelfde method heen.
// Ieder geparsed block wordt onthouden aan een unieke placeholder. Deze wordt later weer teruggeplaatst
// om er zo voor te zorgen dat bij het variabelen vervangen niet binnen dit al verwerkte blok wordt gezocht.
$result = preg_replace_callback('{\<\!\-\-(\s?)loop (\w+?) start(\s?)\-\-\>(.+?)\<\!\-\-(\s?)loop (\\2) end(\s?)\-\-\>}', array($this, 'parse_block'), $template);

// Parse de HTML elementen
$result = preg_replace_callback('{\<([a-zA-Z0-9\"\'\.=\s]+)(.*?)'. $this->atr .'=(\'|\")(\w+)(\\3)(.*?)>(.*?)\</\\1>}', array($this, 'parse_nameholder'), $result);

// Werk van binnen naar buiten. Handel eerst de blokken af (hierboven) en dan pas de variabelen
// De blokken zijn in $result vervangen door variabelen, en in $data zitten nu de geparste blokken als normale variabelen.
//$result = preg_replace_callback('{{}}');
$result = preg_replace_callback('{\{(\w+)\}}', array($this, 'parse_placeholder'), $result);

// Haal de verwijzing naar de data weer weg, zodat we in het omliggende blok weer bij de goeie data komen.
$this->data_stack->pop();

return $result;
}

private function parse_block($matches)
{
list(,, $block_name,, $block_code) = $matches;

$data =& $this->data_stack->top();
$block_data = $data[$block_name];
$results = array();

foreach($block_data as $step_data)
{
$results[] = $this->parse($block_code, $step_data);
}

// Variabele-naam waar we nu het blok in stoppen.
$block_placeholder = sprintf('block%s', uniqid());

// Variabele-naam en de inhoud van dit blok terugstoppen in de data-array
// Dit gaat lukken omdat $data een reference is naar de top van de stack.
$data[$block_placeholder] = implode($results);

return sprintf('{%s}', $block_placeholder);
}

private function parse_nameholder($matches)
{
list(, $tag_name, $a, $separator, $nameholder_name,, $b, $c) = $matches;

$data = $this->data_stack->top();

return '<'. $tag_name . $a . $this->atr .'='. $separator . $nameholder_name . $separator . $b .'>'. $c . $data[$nameholder_name] .'</'. $b .'>';
}

private function parse_placeholder($matches)
{
list(, $placeholder_name) = $matches;

$data = $this->data_stack->top();

return $data[$placeholder_name];
}
}

// Dit is de meest onbegrijpelijke test-data ooit. Trust me.

$template = 'Hoi <div class="boo"></div> {block3} <!-- loop block1 start --> (begin-block1) {var_y} <!-- loop block2 start --> (begin-block2) {var_x} (eind-block2) <!-- loop block2 end --> test2 (eind-block1) <!-- loop block1 end --> Hoi2';

$data = array(
'block1' => array(
0 => array(
'var_y' => 'XXvar_y1XX',
'block2' => array(
0 => array('var_x' => 'XXvar_x11'),
1 => array('var_x' => 'XXvar_x12'),
2 => array('var_x' => 'XXvar_x13'),
)
),
1 => array(
'var_y' => 'XXvar_y2XX',
'block2' => array(
0 => array('var_x' => 'XXvar_x21'),
1 => array('var_x' => '{var_y}'), // Deze verwijst terug naar 'var-y' hierboven. Zou niet moeten!
2 => array('var_x' => 'XXvar_x23'),
)
)
),
'block3' => 'nummer 3',
'boo' => 'boo'
);

$x = new Template($template, false);

echo $x->apply($data);
?>
@SanThe, mijn excuses! Ging niet bewust.
<?php
$this->template = file(implode("", $path));
?>
die regel gaat niet werken ([php]file[/php] geeft een array van regels terug). Probeer eens
<?php
$this->template = file_get_contents($path);
?>
Klopt. Had implode() en file() verkeerd om geschreven. Het nadeel van file_get_contents() is, dat de fopen wrappers ingeschakeld moeten zijn.

<?php
$this->template = implode("", file($path));
?>

Reageren