Currently I have a the following way of retrieving data from my DB:
$school->get('studentCount');
I required a shortcut to access these fields within the page and so came up with a format like this:
<p>Blah blah blah [[studentCount]]</p>
I have output buffering turned on but just need an easy way of replacing that key ('[[field-name]]') with its corresponding data from the DB.
If it was just one field I could do a str_replace on the output like this:
str_replace($output, '[[studentCount]]', $school->get('studentCount'))
Unfortunately that's not suitable. My ideal solution would grab whatever is between '[[' and ']]' and then run the 'get' method and replace the entire key ('[[...]]') with whatever is returned.
Well you could create two arrays, one with the field-name strings [[field-name]] and one with the responses $school->get('field-name'). Then throw those in str_replace as it supports arrays.
Example from PHP Manual:
$phrase = "You should eat fruits, vegetables, and fiber every day.";
$healthy = array("fruits", "vegetables", "fiber");
$yummy = array("pizza", "beer", "ice cream");
$newphrase = str_replace($healthy, $yummy, $phrase);
// Resulting String: "You should eat pizza, beer, and ice cream every day."
If you still wanted to implement your suggestion (finding all [[]]s and replacing them), I'll try to write up a quick function.
Edit: Here are two methods of doing it via your request:
$html = "Hello, [[FirstName]]! Welcome to [[SiteName]].";
$count = preg_match_all("/\[\[([\w]+)\]\]/", $html, $matches);
for ($x = 0; $x < $count; $x++)
$html = str_replace($matches[0][$x], $school->get($matches[1][$x]), $html);
Or using arrays:
$html = "Hello, [[FirstName]]! Welcome to [[SiteName]].";
$count = preg_match_all("/\[\[([\w]+)\]\]/", $html, $matches);
for ($x = 0; $x < $count; $x++)
$matches[1][$x] = $school->get($matches[1][$x]);
$html = str_replace($matches[0], $matches[1], $html);
I'm pretty sure this will work. :)
<?php
// $output contains the string
preg_match_all('/\[{2}([^\[]+)\]{2}/', $output, $matches);
$replaces = $matches['1'];
foreach($replaces as $replace) $str = str_replace('[['.$replace.']]', $school->get($replace), $output);
?>
You will need to use regex to find things inside of two [[ and ]] and take that an insert what is in between into your ->get() function.
Function would be preg_replace
http://us2.php.net/preg-replace
Assuming you can cache the result, a regex and file cache is a great method to do this. First you convert the file:
function cache_it($filename, $tablvar) {
$tmplt = file_get_contents($filename);
$tmplt = preg_replace('/\[\[(.+)\]\]/',
'<?php echo $' . $tablevar . '->get(\1);?>',
$tmplt);
file_put_contents($filename . '.php', $tmplt);
}
Then whenever you need to access the file.
function print_it($filename, $tablevar, $table) {
$_GLOBAL[$tablevar] = $table;
include $filename . '.php';
unset($_GLOBAL[$tablevar]);
}
You probably want to check that the cached file's create date is greater than the last modify date of the source file. Wrapping that and the two functions above in class helps avoid a lot of little pitfalls. But the general idea is sound. There are also some security issues with the cache file being a .php file that you would need to address.
I added this style template caching to the OSS CMS I work on. By caching the regex results, we sped up the original code by over 50%. The real benefit is the templates are PHP files. So anything that speeds the interpreting of PHP files (APC, eAccelerator, etc) speeds up your templates too.
You need to write a parser to look through the $output to find what is between your delimiters and then call a function if it is defined.
I assume you want to do it this way to save the calls until they are needed.
I wrote a template parser that effectively worked this way. Unfortunately it wasn't in PHP.
Thanks for the responses so far. I did think about using str/preg _replace with arrays but I wanted the key ('[[...]]') to directly tie in with the 'get' method, so it's fully expandable. I don't want to have to add to two different arrays every time a new field is added to the DB.
In JavaScript, for example, I would achieve it like this: (JavaScript allows you to pass an anonymous function as the 'replacement' parameter in its equivalent of preg_replace):
('this is the output blah blah [[studentCount]]').replace(/\[{2}([^\[]+)\]{2}/g, function($0, $1) {
get($1);
})
Related
I am creating an OpenCart extension where the admin can change his email templates using the user interface in the admin panel.
I would like the user to have the option to add variables to his custom email templates. For example he could put in:
Hello $order['customer_firstname'], your order has been processed.
At this point $order would be undefined, the user is simply telling defining the message that is to be sent. This would be stored to the database and called when the email is to be sent.
The problem is, how do I get "$order['customer_firstname']" to become a litteral string, and then be converted to a variable when necessary?
Thanks
Peter
If I understand your question correctly, you could do something like this:
The customer has a textarea or similar to input the template
Dear %NAME%, blah blah %SOMETHING%
Then you could have
$values = array('%SOMETHING%' => $order['something'], '%NAME%' => $order['name']);
$str = str_replace(array_keys($values), array_values($values), $str);
the user will be using around 40 variables. Is there a way I can set it to do that for each "%VARIABLE%"?
Yes, you can do so for each variable easily with the help of a callback function.
This allows you, to process each match with a function of your choice, returning the desired replacement.
$processed = preg_replace_callback("/%(\S+)%/", function($matches) {
$name = $matches[1]; // between the % signs
$replacement = get_replacement_if_valid($name);
return $replacement;
},
$text_to_replace_in
);
From here, you can do anything you like, dot notation, for example:
function get_replacement_if_valid($name) {
list($var, $key) = explode(".", $name);
if ($var === "order") {
$order = init_oder(); // symbolic
if(array_key_exists($key, $order)) {
return $order[$key];
}
}
return "<invalid key: $name>";
}
This simplistic implementation allows you, to process replacements such as %order.name% substituting them with $order['name'].
You could define your own simple template engine:
function template($text, $context) {
$tags = preg_match_all('~%([a-zA-Z0-9]+)\.([a-zA-Z0-9]+)%~', $text, $matches);
for($i = 0; $i < count($matches[0]); $i++) {
$subject = $matches[0][$i];
$ctx = $matches[1][$i];
$key = $matches[3][$i];
$value = $context[$ctx][$key];
$text = str_replace($subject, $value, $text);
}
return $text;
}
This allows you to transform a string like this:
$text = 'Hello %order.name%. You have %order.percent%% discount. Pay a total ammount of %payment.ammount% using %payment.type%.';
$templated = template($text, array(
'order' => array(
'name' => 'Alex',
'percent' => 20
),
'payment' => array(
'type' => 'VISA',
'ammount' => '$299.9'
)
));
echo $templated;
Into this:
Hello Alex. You have 20% discount. Pay a total ammount of $299.9 using VISA.
This allows you to have any number of variables defined.
If you want to keep the PHP-syntax, then a regex would be appropriate to filter them:
$text = preg_replace(
"/ [$] (\w+) \[ '? (\w+) \'? \] /exi",
"$$1['$2']", # basically a constrained eval
$text
);
Note that it needs to be executed in the same scope as $order is defined. Else (and preferrably) use preg_replace_callback instead for maximum flexibility.
You could also allow another syntax this way. For example {order[customer]} or %order.customer% is more common and possibly easier to use than the PHP syntax.
You can store it as Hello $order['customer_firstname'] and while accessing make sure you have double-quotes "" to convert the variable to its corresponding value.
echo "Hello $order['customer_firstname']";
Edit: As per the comments, a variation to Prash's answer,
str_replace('%CUSTOMERNAME%', $order['customer_name'], $str);
What you're looking for is:
eval("echo \"" . $input . "\";");
but please, PLEASE don't do that, because that lets the user run any code he wants.
A much better way would be a custom template-ish system, where you provide a list of available values for the user to drop in the code using something like %user_firstname%. Then, you can use str_replace and friends to swap those tags out with the actual values, but you can still scan for any sort of malicious code.
This is why Markdown and similar are popular; they give the user control over presentation of his content while still making it easy to scan for HTML/JS/PHP/SQL injection/anything else they might try to sneak in, because whitelisting is easier than blacklisting.
Perhaps you can have a template like this:
$tpl = "Hello {$order['customer_firstname']}, your order has been processed.".
If $order and that specific key is not null, you can use echo $tpl directly and show the content of 'customer_firstname' key in the text. The key are the curly braces here.
I am developing a php website that needs to be multilingual.
For this reason, I implemented a translation function which has the following header:
function t($string, $replace_pairs = array(), $language = NULL)
Basically, this function is called like this in multiples files of my project:
echo '<p>' . t('Hello world!') . '</p>';
$hello_String = t("Hello #name!", array('#name'=>$username));
I haven't generated the translation strings yet and I would like to generate multiple translation file automatically (one for each language).
What I am looking for is a bash program (or a single command, using grep for example) that would look for every call to this t() function and generate a php file with the following structure:
<?php
/* Translation file "fr.php" */
$strings['fr']['Hello world!'] = '';
$strings['fr']['Hello #name!'] = '';
Has anyone ever encountered this situation and could help me with this ?
Thank you very much.
Kind regards,
Matthieu
Yes, you're not exactly the first to come across this. :)
You can use the venerable gettext system for this, you don't need to invent your own functions. Then you'd get to use xgettext, which is a command line utility to extract strings using the _() function.
If you want to roll your own system for whatever reason, your best bet is to write a PHP script which uses token_get_all to tokenize the source, then go through the tokens and look for T_FUNCTIONs with the value t.
No need to reinvent the wheel
Drupal uses the same t() function for its localization and the potx module is your friend.
If you don't already have, or want to install, a drupal instance you can look at the potx.inc file and reuse it in your script.
Here is the complete API documentation for the translation template extractor.
Try this script http://pastie.org/4568713
Usage:
php script.php ./proj-directory lang1 lang2 lang3
This creates lang1.php, lang2.php, lang3.php files in ./lang directory
You need two functions:
1- scan directories for php files. like this
2- match your t function, grep string and generate the language file. like
function genLang($file) {
$content = file_get_contents($file);
preg_match(...);
foreach(...){
echo(...);
}
}
Yii framework also uses same functionality,
see their MessageCommand class
https://github.com/yiisoft/yii/blob/master/framework/cli/commands/MessageCommand.php#L125
What you need is a (very simple) "template system", but there are two instances of templating in your problem.
Transform "Hello $X!" into "Hello Jonh!" or "Hello Maria!", setting $X. (PHP do this for you in string declarations).
Select the adequate template: "Hello $X!" for english, "¡Hola $X!" for spanish.
The item 1 is the more simple, but the algorithm order is 2,1 (item 2 them item 1).
For this simple task you not need a regular expression (to reinvent the "string with place-holder" of PHP).
Illustrating
For item 1, the simplest way is to declare a specialized function to say "Hello",
// for any PHP version.
function template1($name) { return "<p>Hello $name!</p>";}
print template1("Maria");
For item 2 you need a generalization, that PHP do also for you, by a closure,
header('Content-Type: text/html; charset=utf-8'); // only for remember UTF8.
// for PHP 5.3+. Use
function generalTemplate1($K) {
// $K was a literal constant, now is a customized content.
return function($name) use ($K) {return "<p>$K $name!</p>"; };
}
// Configuring template1 (T1) for each language:
$T1_en = generalTemplate1('Hello'); // english template
$T1_es = generalTemplate1('¡Hola'); // spanish template
// using the T1 multilingual
print $T1_en('Jonh'); // Hello Jonh!
print $T1_es('Maria'); // ¡Hola Maria!
For more templates, use generalTemplate2(), generalTemplate3(), etc.; $T2_en, $T2_es, $T2_fr, $T3_en, $T3_es, etc.
Solution
Now, for pratical use, you like to use arrays... Well, there are a datastructure problem,
and more 1 level of generalization. The cost is variable-name parser for place-holders. I used simple regular expression with preg_replace_callback().
function expandMultilangTemplate($T,$K,$lang,$X) {
// string $T is a template, a HTML structure with $K and $X placeholders.
// array $K is a specific language constants for the template.
// array $lang is the language, a standard 2-letter code. "en", "fr", etc.
// array $X is a set of name-value (compatible with $T placeholders).
// Parsing steps:
$T = str_replace('{#K}',$K[$lang],$T); // STEP-1: expand K into T with lang.
// STEP-2: expand X into T
global $_expMultTpl_X; // need to be global for old PHP versions
$_expMultTpl_X = $X;
$T = preg_replace_callback(
'/#([a-z]+)/',
create_function(
'$m',
'global $_expMultTpl_X;
return array_key_exists($m[1],$_expMultTpl_X)?
$_expMultTpl_X[$m[1]]:
"";
'
),
$T
);
return $T;
}
// CONFIGURING YOUR TEMPLATE AND LANGUAGES:
$T = "<p>{#K} #name#surname!</p>";
$K = array('en'=>'Hello','es'=>'¡Hola');
// take care with things like "!", that is generic, and "¡" that is not.
// USING!
print expandMultilangTemplate(
$T, $K, 'en', array('name'=>'Jonh', 'surname'=>' Smith') );
print expandMultilangTemplate($T, $K, 'es', array('name'=>'Maria'));
I tested this script with PHP5, but it runs with older (PHP 4.0.7+).
About "multilingual files": if your translations are into files, you can use somthing like
$K = getTranslation('translationFile.txt');
function getTranslation($file,$sep='|') {
$K = array();
foreach (file($file) as $line) {
list($lang,$words) = explode($sep,$line);
$K[$lang]=$words;
}
}
and a file as
en|Hello
es|¡Hola
Simplest with PHP 5.3
If you using PHP 5.3+, there are a simple and elegant way to express this "simplest multilingual template system",
function expandMultilangTemplate($T,$K,$lang,$X) {
$T = str_replace('{#K}',$K[$lang],$T);
$T = preg_replace_callback(
'/#([a-z]+)/',
function($m,$X=NULL) use ($X) {
return array_key_exists($m[1],$X)? $X[$m[1]]: '';
},
$T
);
return $T;
}
I have latex + html code somewhere in the following form:
...some text1.... \[latex-code1\]....some text2....\[latex-code2\]....etc
Firstly I want to obtain the latex codes in an array codes[] to be able to send them to a server for rendering, so that
code[0]=latex-code1, code[1]=latex-code2, etc
Secondly, I want to modify this text so that it looks like:
...some text1.... <img src="root/1.png">....some text2....<img src="root/2.png">....etc
i.e, the i-th latex code fragment is replaced by the link to the i-th rendered image.
I have been trying to do this with preg_replace_callback and preg_match_all but being new to PHP haven't been able to make it work. Please advise.
If you're looking for codez:
$html = '...some text1.... \[latex-code1\]....some text2....\[latex-code2\]....etc';
$codes = array();
$count = 0;
$replace = function($matches) use (&$codes, &$count) {
list(, $codes[]) = $matches;
return sprintf('<img src="root/%d.png">', ++$count);
};
$changed = preg_replace_callback('~\\\\\\[(.+?)\\\\\\]~', $replace, $html);
echo "Original: $html\n";
echo "Changed : $changed\n\nLatex Codes: ", print_r($codes, 1), "Count: ", $count;
I don't know at which part you've got the problems, if it's the regex pattern, you use characters inside your markers that needs heavy escaping: For PHP and PCRE, that's why there are so many slashes.
Another tricky part is the callback function because it needs to collect the codes as well as having a counter. It's done in the example with an anonymous function that has variable aliases / references in it's use clause. This makes the variables $codes and $count available inside the callback.
what would be the best way to write a code in Php that would search within a webpage for a number of words stored in a file? is it best to store the source code in a file or is it another way? please help.
The best way is to use google: site:example.com word1 OR word2 OR word3
Do you want to search in ONE PAGE? or one website with MULTIPLE PAGES?
If its only one page i think you can store the html code in memory without problems.
if you know exactly what you search strpos for reach word will probably be the fastest (stripos for case insensitive). you can also define your own character class and use preg_match_all or something... just something like this will do...
<?
$keywords = array("word1","word2","word3");
$doc = strip_tags(file_get_contents("http://www.example.com")); // remove tags to get only text
$doc = preg_replace('/\s+/', ' ',$doc); // remove multiple whitespaces...
foreach($keywords as $word) {
$pos = stripos($doc,$word);
if($pos !== false) {
echo "match: ...".str_replace($word,"<em>$word</em>",substr($doc,$pos-20,50))."... \n";
}
}
?>
something like the following for example will perform MUCH faster as its based on hashmap lookups with O(1) and doesnt need to scan the whole text for every keyword...
<?
setlocale(LC_ALL, "en_US.utf8");
$keywords = array("word1","word2","word3","word4");
$doc = file_get_contents("http://www.example.com");
$doc = strtolower($doc);
$doc = preg_replace('!/\*.*?\*/!s', '', $doc);
$doc = preg_replace("/<!--.*>/i", "", $doc);
$doc = preg_replace('!<script.*?script>!s', '', $doc);
$doc = preg_replace('!<style.*?style>!s', '', $doc);
$doc = strip_tags($doc);
$doc = preg_replace('/[^0-9a-z\s]/','',$doc);
$doc = iconv('UTF-8', 'ASCII//TRANSLIT', $doc); // check if encoding is really utf8
//$doc = preg_replace('{(.)\1+}','$1',$doc); remove duplicate chars ... possible step to add even more fuzzyness
$doc = preg_split("/\s+/",trim($doc));
foreach($keywords as $word) {
$word = strtolower($word);
$word = iconv('UTF-8', 'ASCII//TRANSLIT', $word);
$key = array_search($word,$doc);
var_dump($key);
if($key !== false) {
echo "match: ";
for($i=$key;$i<=5 && isset($doc[$i]);$i++) {
echo $doc[$i]." ";
}
}
}
?>
this code is untested.
it would be however be more elegant to dump textnodes from a domdocument
Simple searching is easy. If you want to search in a whole website the crawling logic is difficult.
I once did a backlink-checker for a company that worked like a crawler.
My first advice is not to do a recursion (like scanning a page and following all links and following all links in that until you reach a certain level...)
rather do it like this:
do a for loop as often as many levels you want to crawl.
set a site array with one entry (start page)
pass array to a function downloads every link, scans the site there and stores links on it in array.
when done with all links return the new link list array
in the for loop update the array with the return value of the function, and call the function again.
this way you can avoid following nasty paths but rather crawl website level by level.
also store already visited links in an array to skip, dont follow external links, check for weird url parameters etc..
for future use you can store documents in lucene or solr, there are classes to turn html pages into senseful lucene objects and search within.
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);
}
}