In PHP is there a better way to do dynamic templating - php

I'm creating module for an application which will have simple custom templating with tags that will be replaced with data from a database. The field names will be different in each instance of this module. I want to know if there is a better way to do this.
The code below is what I've come up with, But I believe there must be a better way. I struggled with preg_split and preg_match_all and just hit my limit so I did it the dumb person way.
<?php
$customTemplate = "
<div>
<<This>>
<<that>>
</div>
";
function process_template ($template, $begin = '<<', $end = '>>') {
$begin_exploded = explode($begin, $template);
if (is_array($begin_exploded)) {
foreach ($begin_exploded as $key1 => $value1) {
$end_exploded = explode($end, $value1);
if (is_array($end_exploded)) {
foreach ($end_exploded as $key2 => $value2) {
$tag = $begin.$value2.$end;
$variable = trim($value2);
$find_it = strpos($template,$tag);
if ($find_it !== false) {
//str_replace ($tag, $MyClass->get($variable), $template );
$template = str_replace ($tag, $variable, $template);
}
}
}
}
}
return $template;
}
echo(process_template($customTemplate));
/* Will Echo
<div>
This
that
</div>
*/
?>
In the future I will connect $MyClass->get() to replace the tag with the proper data. And the custom template will be built by the user.

Rather than preg_split or preg_match I would rather use preg_replace_callback, since you are doing replacements, and the replacement value is derived from what looks like will end up being a method in another class.
function process_template($template, $begin = '<<', $end = '>>') {
// get $MyClass in the function scope somehow. Maybe pass it as another parameter?
return preg_replace_callback("/$begin(\w+)$end/", function($var) use ($MyClass) {
return $MyClass->get($var[1]);
}, $template);
}
Here's an example to play with: https://3v4l.org/N1p03
I assume this is just for fun/learning. If I really needed to use a template for something I would rather start with composer require "twig/twig:^2.0" instead. In fact, if you're interested in learning more about how it works you could go check out a well-established system like twig or blade does it. (Better than I've done it in this answer.)

There are tons templating engines around, but sometimes... just add complexity and dependencies for a maybe simple thing. This a modified sample of what I used for make some javascript corrections. This works for your template.
function process_template($html,$b='<<',$e='>>'){
$replace=['this'=>'<input name="this" />','that'=>'<input name="that" />'];
if(preg_match_all('/('.$b.')(.*?)('.$e.')/is',$html,$matches,PREG_SET_ORDER|PREG_OFFSET_CAPTURE)){
$t='';$o=0;
foreach($matches as $m){
//for reference $m[1][0] contains $b, $m[2][0] contains $e
$t.=substr($html,$o,$m[0][1]-$o);
$t.=$replace[$m[2][0]];
$o=$m[3][1]+strlen($m[3][0]);
}
$t.=substr($html,$o);
$html=$t;
}
return $html;
}
$html="
<div>
<<this>>
<<that>>
</div>
";
$new=process_template($html);
echo $new;
For demo purpose I put the array $replace that handling the substitutions. You replace those with your function that will handle the replacement.
Here is a working snippet: https://3v4l.org/MBnbR
I like this function because you have control of what to replace and what to put back on the final result. By the way by using the PREG_OFFSET_CAPTURE also return on the matches the position where the regexp groups happens. Those are on the $m[x][1]. The captured text will be on $m[x][0].

Related

How do I insert html code into a php regex?

So I am currently in the middle of making a forums software. Something that I wanted for that forums software was a custom template engine. For the most part I have created the template engine, but I am having a small issue with the regex that I use for my IF, ELSEIF, and FOREACH statements.
The issue that I am having is that when I put a chunk of html code in to my regex, nothing will work. Here is an example: https://regex101.com/r/jlawz3/1.
Here is the PHP code that checks for the regex.
$isMatchedAgain = preg_match_all('/{IF:(.*?)}[\s]*?(.*?)[\s]*?{ELSE}[\s]*?(.*?)[\s]*?{ENDIF}/', $this->template, $elseifmatches);
for ($i = 0; $i < count($elseifmatches[0]); $i++) {
$condition = $elseifmatches[1][$i];
$trueval = $elseifmatches[2][$i];
$falseval = (isset($elseifmatches[3][$i])) ? $elseifmatches[3][$i] : false;
$res = eval('return ('.$condition.');');
if ($res===true) {
$this->template = str_replace($elseifmatches[0][$i],$trueval,$this->template);
} else {
$this->template = str_replace($elseifmatches[0][$i],$falseval,$this->template);
}
}
You can do it like this:
function render($content) {
$match = preg_match_all('/{IF:\((.*?)\)}(.*?){ELSE}(.*?)({ENDIF})/s', $content, $matches, PREG_OFFSET_CAPTURE);
if (!$match) {
return $content;
}
$beforeIf = substr($content, 0, $matches[0][0][1]);
$afterIf = substr($content, $matches[4][0][1] + strlen('{ENDIF}'));
$evalCondition = eval('return (' . $matches[1][0][0] . ');');
if ($evalCondition) {
$ifResult = $matches[2][0][0];
} else {
$ifResult = $matches[3][0][0];
}
return
$beforeIf .
$ifResult .
render($afterIf);
}
Working example.
This is a first step. This wont work for example if you have an if within an if.
Talking about mentioned security risk. Since we are using eval (nickname EVIL - for a reason). You should never ever ever process user-input through eval - or use eval at all - there is always a better solution.
For me it looks like you want to give users the ability to write "code" in their posts. If this is the case you can have a look at something like bbcode.
Whatever you do be sure to provide the desired functionality. Taking your example:
!isset($_SESSION['loggedin'])
You could do something like this:
{IS_LOGGED_IN}
Output whatever you want :)
{/IS_LOGGED_IN}
Your renderer would look specificly for this tag and act accordingly.
So after a bit of research, I have figured out that the issue I was facing could be solved by adding /ims to the end of my regex statement. So now my regex statement looks like:
/{IF:(.*?)}[\s]*?(.*?)[\s]*?{ELSE}[\s]*?(.*?)[\s]*?{ENDIF}/ims

PHP tag replacement method

I am storing HTML layouts within a MySQL database. These layouts may contain tags within the HTML as show below..
{site.poll="fred,joe,john"}
and
{site.layout.header}
Currently i am searching the HTML template by executing multiple preg_matches to identify the tags, looping through the array then executing a str_replace(), replacing with another partial html template also pulled back from the db.. Example below..
if (preg_match_all('/{site\.layout\.(.)*}/', $data, $match) != FALSE)
{
foreach($match[0] as $value)
{
$value = trim($value, '{}');
$tmp_store = explode('.', $value);
$tmp_partial = $this->parse($this->get_layout(end($tmp_store)));
$data = str_replace('{'. $value .'}', $tmp_partial, $data);
}
}
I would need to execute a regex for each tag i required, then execute a str_replace on each instance of that tag.. The same again would need doing for each required partial template..
To me, this is all seeming to get heavy..
Is there a better way of doing this?
Thanks in advance..
Edit: I do not want to use an existing library, i would like to do this task myself and learn in the process..
Well you could use preg_replace to find and replace your tags in one shot.
The best approach in my opinion would be to use an existing template system such as Twig or Smarty. I know for sure that you can read data into Smarty (it doesn't have to be from a file). I'm sure Twig has something similar.
Twig and Smarty also provide caching options so you aren't rebuilding your template on every request. However they work best if the templates are stored in files.
If you really must roll your own template system you should build some kind of parser that actually checks the content character by character. This will likely be faster and more accurate than regular expressions (though more complex)
I don't expect this to answer your question as such, but thought it might give you something else to think about. Some code I've got for when my template class is overkill.
function replace_tags(&$xhtml, $tags) {
if( is_array($tags) && count($tags) > 0 )
foreach ($tags as $tag => $data) {
$xhtml = str_replace("{{" . $tag . "}}", $data, $xhtml);
}
if( $xhtml ) return $xhtml;
}
$tpl = "/templates/index.xhtml";
$tags = array(
"css" => null,
"js" => null,
"main_content" => null
);
$tags['main_content'] = file_get_contents("/home/main.xhtml");
echo replace_tags(file_get_contents($tpl), $tags);
edit
Thought I'd clarify on the reason the function receives $xhtml by reference, and also returns $xhtml. Basically just to make it dual purpose.
//Usage at the end of a page
echo replace_tags(file_get_contents($tpl), $tags);
//Usage for template in template
$tags['menu'] = file_get_contents($menu_tpl);
replace_tags($tags['menu'], $tags);
echo replace_tags(file_get_contents($tpl), $tags);

Replacing delimiters with PHP variables

I'm writing a mail class that pulls content stored in a database and loads it into a template before sending it as a HTML e-mail. However, because each e-mail contains PHP variables and dynamic content, I've decided to use delimiters. So instead of the content looking like:
Hello $username, welcome to the site.
It'll look like:
Hello {{username}}, welcome to the site.
So far I'm using these methods:
function load($name,$content)
{
// preps the template for HTML
}
function content($template_id)
{
$template = $this->db->get_where('email_templates',array('id'=>$template_id));
return $template->content;
}
function new_email($email,$name,$user_type)
{
$msg = $this->load($name,$this->content(1));
$this->send($email,'Thanks for your application',$msg,1);
}
The trouble I'm having is how to convert a {{variable}} into a $variable so that it will parse - I don't want it to just be loaded as $username in the e-mail template. Is it just a case of using regular expressions and escaping the string so that it'll parse? Something like:
$content = str_replace("{{","'.$",$template->content);
$content = str_replace("}}",".'",$template->content);
Or is this flawed? Does anybody know what's the best thing to do?
I would not create my own templating system, because there are existing ones out there.
The most popular is probably Smarty, but there is an another one which has the same format as you created, that is mustache.
Update:
The problem with your code is that you're replacing the {{ to a .$ and store that in $content variable, then replacing }} to . and overwrite this replaced $content variable.
A possible working solution could be:
if (preg_match_all("/{{(.*?)}}/", $template, $m)) {
foreach ($m[1] as $i => $varname) {
$template = str_replace($m[0][$i], sprintf('$%s', $varname), $template);
}
}
But then you would also need to eval your code somehow.
So after converting {{variable}} to $variable in your email template, you will use eval to get it replaced by the actual contents of that variable?
Why not just replace {{variable}} with the contents of $variable straight away?
Perhaps have a function that takes the template text and an array of placeholder => "text to replace it with". Then it's as simple as making up the placeholders' exact strings by adding {{ and }} around that array's key and doing str_replace.
foreach ($replacements as $placeholder => $value) {
$placeholder = "{{" . $placeholder . "}}" ;
$text = str_replace($placeholder, $value, $text) ;
}
Couple this with (class) constants for the placeholders and you have a very solid and typo-repelant templating system. It will not be as elegant or easy to use as a full blown templating solution, and it might require extra work from whoever writes code that uses it, but they will not make mistakes during development due to mis-named variables.
If you are going to do it yourself it is probably best to just be explicit with str_replace. If you try to convert the curly bracers to $ you'll then need to eval() which is a potential security hole.
This would be my approach with str_replace - this becomes difficult to maintain as you add more variables but it really doesn't get much simpler either.
$content = str_replace(
array('{{username}}','{{var2}}'),
array($username,$var2),
$template->content
);
use preg_replace_callback , see : http://codepad.org/EvzwTqzJ
<?php
$myTemplateStr = "Hello {{username}} , this is {{subject}} ,and other string {{example}}";
$tagRegex = "|{{(.*?)}}|is";
$result = preg_replace_callback($tagRegex,"myReplaceFunc",$myTemplateStr);
echo $result ;
/* output :
Hello $username , this is $subject ,and other string {{example}}
*/
function myReplaceFunc($matches)
{
$validTags = array('username','subject','name');
$theFull = $matches[0];
$theTag = $matches[1];
if(in_array($theTag,$validTags) == true)
return '$'.$theTag;
return $theFull ;
}
?>
$template = "Hello {{username}} , this is {{subject}} ,and other the answer is on page {{example}}";
$replacements = array(
'username' => 'Jeffrey',
'subject' => 'your final notice',
'page' => 43
);
function bind_to_template($replacements, $template) {
return preg_replace_callback('/{{(.+?)}}/',
function($matches) use ($replacements) {
return $replacements[$matches[1]];
}, $template);
}
echo bind_to_template($replacements, $template);
Credit to https://www.labnol.org/code/19266-php-templates

super-light templating system in PHP that doesn't allow php code inside templates or use eval

I'm searching for a very basic PHP templating system. Right now I'm using:
/**
* Renders a single line. Looks for {{ var }}
*
* #param string $string
* #param array $parameters
*
* #return string
*/
function renderString($string, array $parameters)
{
$replacer = function ($match) use ($parameters)
{
return isset($parameters[$match[1]]) ? $parameters[$match[1]] : $match[0];
};
return preg_replace_callback('/{{\s*(.+?)\s*}}/', $replacer, $string);
}
(from here: PHP - Extremely light templating system)
but I can only assign and display variables. I also need a way to use conditions like IF and loop arrays.
I found Rain TPL - http://www.raintpl.com/Quick-Start/#if - which is very close to what I'm looking for, but there are a few things that I don't like it it:
it allows the dude who is writing the template to run PHP functions (inside the IF condition).
it writes cache and php files, which I don't want
So, is there anything out there similar to this, but even more "basic", strict, and more secure?
Twig might be for you.
It can do conditions, and has a sandbox mode for untrusted code.
It does compilation and caching, but that seems to be possible to turn off.
There's also a Mustache port for PHP. The PHP port is here. The syntax is similar to what you're already doing, and supports simple IF and FOREACH-type loops.
And, does it without eval.
Have a look at Twig or H2O.
http://www.twig-project.org/
http://www.h2o-template.org/
From your requirements I am guessing you are wanting your website users to write some basic php scripts. You might not find a free template engine that does that.
I think it's better for you if you change an existing template engine to your needs.
You can change Rain TPL to disable some of its features that you don't want. For example you can do...
Disable function use in IF statements:
a. Locate elseif( preg_match( '/\{if(?: condition){0,1}="([^"]*)"\}/', $html, $code ) ){
b. Replace $this->function_check( $tag ); with a new method something like $this->ifcondition_function_check( $tag );
c. Create the new method that will disable all functions in IF statements.
private function ifcondition_function_check($code)
{
$preg = '/[a-zA-z0-9]+\((.*?)\)/';
if (preg_match( $preg, $code, $match ) ){
// find the line of the error
$line = 0;
$rows=explode("\n",$this->tpl['source']);
while( !strpos($rows[$line],$code) )
$line++;
// draw the error line
$error = str_replace( array('<','>'), array( '<','>' ), array($code,$rows[$line]) );
$error = str_replace( $code, "<font color=red>$code</font>", $rows[$line] );
// debug the error and stop the execution of the script
die( "<div>RainTPL Sandbox Error in template <b>{$this->tpl['tpl_filename']}</b> at line $line : <i>$error</i></b>" );
}
}
d. Now functions are disabled.
Remove the cache file. (The cache file in Rain TPL is a PHP file with the template tags replaced by PHP code)
a. Go to method draw()
b. Locate unset( $this->tpl );
c. Just before this line remove the complied (cache) file #unlink($this->tpl['compiled_filename']);.
d. Now the cache file is just a temporary file to execute the PHP code.
Hope this helps
very easy to use
http://www.smarty.net/
When you want it really small and flexible maybe the best is to stay with your own stuff? I like handcrafting ;-) You can extend your existing function. Following, your function plus if and loop statement and escaping of variables for security:
<?php
function renderString($str, $parms)
{
// if
$str = preg_replace_callback('/{{if (?P<name>\w+)}}(?P<inner>.*?){{endif}}/is', function($match) use ($parms) {
if( isset($parms[$match['name']])) {
// recursive
return renderString($match['inner'], $parms);
}
}, $str);
// loop
$str = preg_replace_callback('/{{loop (?P<name>\w+)}}(?P<inner>.*?){{endloop}}/is', function($match) use ($parms) {
if( isset($parms[$match['name']]) && is_array($parms[$match['name']])) {
$str = '';
foreach ($parms[$match['name']] as $value) {
$parms['loop'] = $value;
// recursive
$str .= renderString($match['inner'], $parms);
}
return $str;
}
}, $str);
// var
$str = preg_replace_callback('/{{(?P<name>\w+)}}/is', function($match) use ($parms) {
if( isset($parms[$match['name']])) {
return htmlspecialchars($parms[$match['name']]);
}
}, $str);
return $str;
}
$template = "<h1>{{title}}</h1>
{{if optional}}
<p>Optional: {{optional}}</p>
{{endif}}
{{if noop}}I'm not there{{endif}}
<ul>
{{loop numbers}}
<li>{{symbol}} {{loop}}</li>
{{endloop}}
</ul>";
echo renderString($template, array(
'title' => 'The Title',
'optional' => 'I am optional',
'numbers' => array( 'one', 'two', 'three'),
'symbol' => '>',
));
This script is tested in PHP 5.3 and you can copy it 1:1 to a file to play with it.
try PHPTAL: http://phptal.org/
the syntax for TAL templates does not break html, so you - and the designers can check if they going to look good.
see also:
http://wiki.zope.org/ZPT/TALSpecification14
http://wiki.zope.org/ZPT/TAL

Need a regex to add css class to first and last list item

UPDATE:
Thank you all for your input. Some additional information.
It's really just a small chunk of markup (20 lines) I'm working with and had aimed to to leverage a regex to do the work.
I also do have the ability to hack up the script (an ecommerce one) to insert the classes as the navigation is built. I wanted to limit the number of hacks I have in place to keep things easier on myself when I go to update to the latest version of the software.
With that said, I'm pretty aware of my situation and the various options available to me. The first part of my regex works as expected. I posted really more or less to see if someone would say, "hey dummy, this is easy just change this....."
After coming close with a few of my efforts, it's more of the principle at this point. To just know (and learn) a solution exists for this problem. I also hate being beaten by a piece of code.
ORIGINAL:
I'm trying to leverage regular expressions to add a CSS a class to the first and last list items within an ordered list. I've tried a bunch of different ways but can't produce the results I'm looking for.
I've got a regular expression for the first list item but can't seem to figure a correct one out for the last. Here is what I'm working with:
$patterns = array('/<ul+([^<]*)<li/m', '/<([^<]*)(?<=<li)(.*)<\/ul>/s');
$replace = array('<ul$1<li class="first"','<li class="last"$2$3</ul>');
$navigation = preg_replace($patterns, $replace, $navigation);
Any help would be greatly appreciated.
Jamie Zawinski would have something to say about this...
Do you have a proper HTML parser? I don't know if there's anything like hpricot available for PHP, but that's the right way to deal with it. You could at least employ hpricot to do the first cleanup for you.
If you're actually generating the HTML -- do it there. It looks like you want to generate some navigation and have a .first and .last kind of thing on it. Take a step back and try that.
+1 to generating the right html as the best option.
But a completely different approach, which may or may not be acceptable to you: you could use javascript.
This uses jquery to make it easy ...
$(document).ready(
function() {
$('#id-of-ul:firstChild').addClass('first');
$('#id-of-ul:lastChild').addClass('last');
}
);
As I say, may or may not be any use in this case, but I think its a valid solution to the problem in some cases.
PS: You say ordered list, then give ul in your example. ol = ordered list, ul = unordered list
You wrote:
$patterns = array('/<ul+([^<]*)<li/m','/<([^<]*)(?<=<li)(.*)<\/ul>/s');
First pattern:
ul+ => you search something like ullll...
The m modifier is useless here, since you don't use ^ nor $.
Second pattern:
Using .* along with s is "dangerous", because you might select the whole document up to the last /ul of the page...
And well, I would just drop s modifier and use: (<li\s)(.*?</li>\s*</ul>) with replace: '$1class="last" $2'
In view of above remarks, I would write the first expression: <ul.*?>\s*<li
Although I am tired of seeing the Jamie Zawinski quote each time there is a regex question, Dustin is right in pointing you to a HTML parser (or just generating the right HTML from the start!): regexes and HTML doesn't mix well, because HTML syntax is complex, and unless you act on a well known machine generated output with very predictable result, you are prone to get something breaking in some cases.
I don't know if anyone cares any longer, but I have a solution that works in my simple test case (and I believe it should work in the general case).
First, let me point out two things: While PhiLho is right in that the s is "dangerous", since dots may match everything up to the final of the document, this may very well be what you want. It only becomes a problem with not well formed pages. Be careful with any such regex on large, manually written pages.
Second, php has a special meaning of backslashes, even in single quotes. Most regexen will perform well either way, but you should always double-escape them, just in case.
Now, here's my code:
<?php
$navigation='<ul>
<li>Coffee</li>
<li>Tea</li>
<li>Milk</li>
<li>Beer</li>
<li>Water</li>
</ul>';
$patterns = array('/<ul.*?>\\s*<li/',
'/<li((.(?<!<li))*?<\\/ul>)/s');
$replace = array('$0 class="first"',
'<li class="last"$1');
$navigation = preg_replace($patterns, $replace, $navigation);
echo $navigation;
?>
This will output
<ul>
<li class="first">Coffee</li>
<li>Tea</li>
<li>Milk</li>
<li>Beer</li>
<li class="last">Water</li>
</ul>
This assumes no line feeds inside the opening <ul...> tag. If there are any, use the s modifier on the first expression too.
The magic happens in (.(?<!<li))*?. This will match any character (the dot) that is not the beginning of the string <li, repeated any amount of times (the *) in a non-greedy fashion (the ?).
Of course, the whole thing would have to be expanded if there is a chance the list items already have the class attribute set. Also, if there is only one list item, it will match twice, giving it two such attributes. At least for xhtml, this would break validation.
You could load the navigation in a SimpleXML object and work with that. This prevents you from breaking your markup with some crazy regex :)
As a preface .. this is waaay over-complicating things in most use-cases. Please see other answers for more sanity :)
Here is a little PHP class I wrote to solve a similar problem. It adds 'first', 'last' and any other classes you want. It will handle li's with no "class" attribute as well as those that already have some class(es).
<?php
/**
* Modify list items in pre-rendered html.
*
* Usage Example:
* $replaced_text = ListAlter::addClasses($original_html, array('cool', 'awsome'));
*/
class ListAlter {
private $classes = array();
private $classes_found = FALSE;
private $count = 0;
private $total = 0;
// No public instances.
private function __construct() {}
/**
* Adds 'first', 'last', and any extra classes you want.
*/
static function addClasses($html, $extra_classes = array()) {
$instance = new self();
$instance->classes = $extra_classes;
$total = preg_match_all('~<li([^>]*?)>~', $html, $matches);
$instance->total = $total ? $total : 0;
return preg_replace_callback('~<li([^>]*?)>~', array($instance, 'processListItem'), $html);
}
private function processListItem($matches) {
$this->count++;
$this->classes_found = FALSE;
$processed = preg_replace_callback('~(\w+)="(.*?)"~', array($this, 'appendClasses'), $matches[0]);
if (!$this->classes_found) {
$classes = $this->classes;
if ($this->count == 1) {
$classes[] = 'first';
}
if ($this->count == $this->total) {
$classes[] = 'last';
}
if (!empty($classes)) {
$processed = rtrim($matches[0], '>') . ' class="' . implode(' ', $classes) . '">';
}
}
return $processed;
}
private function appendClasses($matches) {
array_shift($matches);
list($name, $value) = $matches;
if ($name == 'class') {
$value = array_filter(explode(' ', $value));
$value = array_merge($value, $this->classes);
if ($this->count == 1) {
$value[] = 'first';
}
if ($this->count == $this->total) {
$value[] = 'last';
}
$value = implode(' ', $value);
$this->classes_found = TRUE;
}
return sprintf('%s="%s"', $name, $value);
}
}

Categories