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);
Related
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].
im using this php code (from a question on here):
<?php
define(TEMPLATES_LOCATION, 'templates/');
function TemplateFunction ($template, $replaces) {
$template = file_get_contents(TEMPLATES_LOCATION . $template);
if (is_array($replaces)) {
foreach($replaces as $replacekey => $replacevalue){
$template = str_replace('{$' . $replacekey . '}', $replacevalue, $template);
}
}
return $template;
}
$keys = array(
'TITLE' => 'This is page title',
'HEADER' => 'This is some header',
'GALLERY' => 'php while loop here'
);
echo TemplateFunction('body.tpl', $keys);
?>
The way my template file is set up is the following HTML:
<body>
<div id="gallery">{GALLERY}</div>
</body>
so where {GALLERY} is, the php script should replace that with my automatically generated <li><img src="images/'.$filename.'"/></li> which is being run in a while loop generated from a mysql request
what i thought might work is:
$keys = array(
'TITLE' => 'This is page title',
'HEADER' => 'This is some header',
'GALLERY' => 'while($row = mysql_fetch_array($result)){<li><img src="'.$row['filename'].'"/></li>})'
);
but it doesnt :(
Re your code:
$keys = array(
'TITLE' => 'This is page title',
'HEADER' => 'This is some header',
'GALLERY' => 'while($row = mysql_fetch_array($result)){<li><img src="'.$row['filename'].'"/></li>})'
);
I would consider it very poor practice to have PHP code embedded in your template data this way. Allow me to offer you a completely different solution...
Firstly, we need to encapsulate the code. The code you've shown above is trying to fetch data from a $result variable, but $result itself is obviously been set elsewhere. This isn't good, because if we ever want to change the way the gallery feature works, we would need to search all over the code to find different bits of it.
Instead, you should write the feature as a self-contained function (or a class if it's too complex for a single function), which would load the data, and process it.
Let's call that function loadGallery(). It might look something like this:
function loadGallery() {
$output = '';
$result = mysql_query('...gallery query here...');
while($row = mysql_fetch_array($result)) {
$output .= "<li><img src='{$row['filename']}'/></li>";
}
return '<ul>'.$output.'</ul>';
}
Obviously, it could be a lot more complex than that (eg you may want to paginate it, or offer category options; these would be written as parameters for the function). I've also not written any error checking, etc here for brevity.
Now that you've got that function, you can plug it into the template engine. You need to reference the function in your template.
You currently have template markers like this {TITLE}. In your code, these markers are swapped with plain text using str_replace(). That's fine. But in this case we now want to call a function instead, so we need to have a different kind of marker.
Let's write the gallery marker like this: {FUNCTION:loadGallery} instead of {GALLERY} as you currently have.
Now you can have an additional bit of code in your template engine that looks at these {function:} markers and replaces them with a function call. (You can keep the existing simple str_replace() method as well of course)
$funcReplaces = array( //similar to your existing $keys array, but for allowed functions
'loadGallery'
);
foreach($funcReplaces as $replaceFunc){
$template = str_replace('{function:' . $replaceFunc . '}', $replaceFunc(), $template);
}
This will run the function and put the output into the template.
So that answers the question for you.
What I should point out, however, is that there are a lot of other issues that you need to think about when writing a templating engine, from both a technical and security perspective. The above describes a basic way to resolve the specific question at hand, but it isn't a fantastic all-singing all-dancing template engine. It's still just a pretty basic one.
That's fine, if that's all you need, but if you expect this system to grow, it's worth noting that this whole area is that this is very much a problem that others have already solved, and solved well. There are several very good templating engines available for PHP. I would suggest that your best course of action would be to investigate some of them, and maybe use one of those instead of writing your own.
Some that you could try:
Smarty
Twig
Mustache
Hope that helps.
You cannot "execute PHP code within an array". You simply build the array programmatically:
while (/* something */) {
$keys['GALLERY'][] = $something;
}
I have found the following code that i've written to be the right solution to my issue:
$gallery = array();
while($row = mysql_fetch_assoc($result))
{
$gallery[] = '<li><img src="'.$row['gallery_image_filename'].'" width="'.$final_image_width.'" height="auto" style="margin: '.$other_margin.'px '.$gallery_image_margin.'px '.$other_margin.'px 0px"/></li>';
}
$gallery = implode(' ', $gallery);
i then can concatenate my $gallery variable into the 'GALLERY' => 'value' value and it outputs all the database results as required. Thanks to all for your help :)
Well, you can create an class with __toString magic method which will process all images on your gallery and then return the string.
class TemplateGallery{
private $images = array();
public function addImage($src){
$images[] = $src;
}
public function __toString(){
$str = "";
foreach($images as $image){
$str .= sprintf('<li><img src="%s"></li>',$image);
}
return $str;
}
}
Well, as the guy in comments said, it's too "hard", so, you can use an "pre-process" function, that will process your images array, and then return your template string.
$images = array(/* Put images src here */);
function processGallery($images){
$str = "";
foreach($images as $image){
$str .= sprintf('<li><img src="%s"></li>',$image);
}
return $str;
}
And then call that function on your array.
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
The site I'm working on has a database table filled with glossary terms. I am building a function that will take some HTML and replace the first instances of the glossary terms with tooltip links.
I am running into a problem though. Since it's not just one replace, the function is replacing text that has been inserted in previous iterations, so the HTML is getting mucked up.
I guess the bottom line is, I need to ignore text if it:
Appears within the < and > of any HTML tag, or
Appears within the text of an <a></a> tag.
Here's what I have so far. I was hoping someone out there would have a clever solution.
function insertGlossaryLinks($html)
{
// Get glossary terms from database, once per request
static $terms;
if (is_null($terms)) {
$query = Doctrine_Query::create()
->select('gt.title, gt.alternate_spellings, gt.description')
->from('GlossaryTerm gt');
$glossaryTerms = $query->rows();
// Create whole list in $terms, including alternate spellings
$terms = array();
foreach ($glossaryTerms as $glossaryTerm) {
// Initialize with title
$term = array(
'wordsHtml' => array(
h(trim($glossaryTerm['title']))
),
'descriptionHtml' => h($glossaryTerm['description'])
);
// Add alternate spellings
foreach (explode(',', $glossaryTerm['alternate_spellings']) as $alternateSpelling) {
$alternateSpelling = h(trim($alternateSpelling));
if (empty($alternateSpelling)) {
continue;
}
$term['wordsHtml'][] = $alternateSpelling;
}
$terms[] = $term;
}
}
// Do replacements on this HTML
$newHtml = $html;
foreach ($terms as $term) {
$callback = create_function('$m', 'return \'<span>\'.$m[0].\'</span>\';');
$term['wordsHtmlPreg'] = array_map('preg_quote', $term['wordsHtml']);
$pattern = '/\b('.implode('|', $term['wordsHtmlPreg']).')\b/i';
$newHtml = preg_replace_callback($pattern, $callback, $newHtml, 1);
}
return $newHtml;
}
Using Regexes to process HTML is always risky business. You will spend a long time fiddling with the greediness and laziness of your Regexes to only capture text that is not in a tag, and not in a tag name itself. My recommendation would be to ditch the method you are currently using and parse your HTML with an HTML parser, like this one: http://simplehtmldom.sourceforge.net/. I have used it before and have recommended it to others. It is a much simpler way of dealing with complex HTML.
I ended up using preg_replace_callback to replace all existing links with placeholders. Then I inserted the new glossary term links. Then I put back the links that I had replaced.
It's working great!
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);
}
}