I'm required to create a simple template engine; I can't use Twig or Smarty, etc. because the designer on the project needs to be able to just copy/paste her HTML into the template with no configuration, muss/fuss, whatever. It's gotta be really easy.
So I created something that will allow her to do just that, by placing her content between {{ CONTENT }} {{ !CONTENT }} tags.
My only problem is that I want to make sure that if she uses multiple spaces in the tags - or NO spaces - it won't break; i.e. {{ CONTENT }} or {{CONTENT}}
What I have below accomplishes this, but I'm afraid it may be overkill. Anybody know a way to simplify this function?
function defineContent($tag, $string) {
$offset = strlen($tag) + 6;
// add a space to our tags if none exist
$string = str_replace('{{'.$tag, '{{ '.$tag, $string);
$string = str_replace($tag.'}}', $tag.' }}', $string);
// strip consecutive spaces
$string = preg_replace('/\s+/', ' ', $string);
// now that extra spaces have been stripped, we're left with this
// {{ CONTENT }} My content goes here {{ !CONTENT }}
// remove the template tags
$return = substr($string, strpos($string, '{{ '.$tag.' }}') + $offset);
$return = substr($return, 0, strpos($return, '{{ !'.$tag.' }}'));
return $return;
}
// here's the string
$string = '{{ CONTENT }} My content goes here {{ !CONTENT }}';
// run it through the function
$content = defineContent('CONTENT', $string);
echo $content;
// gives us this...
My content goes here
EDIT
Ended up creating a repo, for anyone interested.
https://github.com/timgavin/tinyTemplate
I would suggest to take a look at variable extraction into the template scope.
It's a bit easier to maintain and less overhead, than the replace approach and its often easier to use for the designer. In its basic form, its just PHP variables and short tags.
It depends on which side you generate, e.g. a table and its rows (or complete content blocks) - it could be just <?=$table?> ;) Less work for the designer, more work for you. Or just provide a few rendering examples and helpers, because copy/pasting examples should always work, even with an untrained designer.
Template
The template is just HTML mixed with <?=$variable?> - uncluttered.
src/Templates/Article.php
<html>
<body>
<h1><?=$title?></h1>
<div><?=$content?></div>
</body>
</html>
Usage
src/Controller/Article.php
...
// initalize
$view = new View;
// assign
$view->data['title'] = 'The title';
$view->data['content'] = 'The body';
// render
$view->render(dirname(__DIR__) . '/Templates/Article.php');
View / TemplateRenderer
The core function here is render(). The template file is included and the variable extraction happens in a closure to avoid any variable clashes/scope problems.
src/View.php
class View
{
/**
* Set data from controller: $view->data['variable'] = 'value';
* #var array
*/
public $data = [];
/**
* #var sting Path to template file.
*/
function render($template)
{
if (!is_file($template)) {
throw new \RuntimeException('Template not found: ' . $template);
}
// define a closure with a scope for the variable extraction
$result = function($file, array $data = array()) {
ob_start();
extract($data, EXTR_SKIP);
try {
include $file;
} catch (\Exception $e) {
ob_end_clean();
throw $e;
}
return ob_get_clean();
};
// call the closure
echo $result($template, $this->data);
}
}
Answering specifically what you asked:
My only problem is that I want to make sure that if she uses multiple spaces in the tags - or NO spaces - it won't break
What I have below accomplishes this, but I'm afraid it may be overkill. Anybody know a way to simplify this function?
... the only "slow" part of your function is the preg_replace. Use trim instead, for a very slight increase in speed. Otherwise, don't worry about it. There's no magic PHP command to do what you're looking to do.
Related
I'm using Twig's markdown_to_html filter, and it works very well.
However, in some use cases, I'd want it to generate HTML, but without the paragraph tags.
For instance, from this Markdown content:
Hello, this is **some Markdown**
I want the exported HTML to be:
Hello, this is <strong>some Markdown</strong>
But the result is currently:
<p>Hello, this is <strong>some Markdown</strong></p>
I looked into the filter's source and didn't se any option to do so.
Is there a way to do this, or should I create my own Twig filter?
I'd prefer to avoid the striptags filter if possible, because I don't want to list all the tags I'll allow (unless there is a reverse striptags where you can specify the tags you want removed ?)
It looks like you're using league/commonmark which has an "Inlines Only" extension for this exact purpose! It will avoid outputting block-level elements like paragraphs, headers, etc. - only things like emphasis and links would be rendered as HTML.
To use it, construct your Markdown converter like this:
<?php
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\InlinesOnly\InlinesOnlyExtension;
use League\CommonMark\MarkdownConverter;
// Define your configuration, if needed
$config = [];
// Create a new, empty environment
$environment = new Environment($config);
// Add this extension
$environment->addExtension(new InlinesOnlyExtension());
// Instantiate the converter engine and start converting some Markdown!
$converter = new MarkdownConverter($environment);
echo $converter->convert('Hello, this is **some Markdown**');
This will be more reliable than parsing HTML with regular expressions.
(I'm the maintainer of league/commonmark and would be happy to answer any follow-up questions you might have in the comments)
I updated my filter, following Colin O'Dells answer. This way, it is more robust, and it will allow the usage or creation of more CommonMark extensions in the future.
# src\Twig\CustomTwigExtension.php
<?php
declare(strict_types=1);
namespace App\Twig;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\ConfigurableExtensionInterface;
use League\CommonMark\Extension\InlinesOnly\InlinesOnlyExtension;
use League\CommonMark\MarkdownConverter;
use Twig\Extension\AbstractExtension;
use Twig\Markup;
use Twig\TwigFilter;
use Webmozart\Assert\Assert;
class CustomTwigExtension extends AbstractExtension
{
private const CONFIG = [
'default' => [CommonMarkCoreExtension::class],
'inline' => [InlinesOnlyExtension::class],
];
/**
* #return TwigFilter[]
*/
public function getFilters(): array
{
return [
new TwigFilter('custom_markdown', [$this, 'customMarkdown']),
];
}
public function customMarkdown(string $str, string $configName = 'default'): Markup
{
$env = new Environment();
foreach ($this->getExtensions($configName) as $extension) {
$env->addExtension($extension);
}
$converter = new MarkdownConverter($env);
$html = $converter->convert($str)->getContent();
return new Markup($html, 'UTF-8');
}
/**
* #return ConfigurableExtensionInterface[]
*/
private function getExtensions(string $configName): array
{
Assert::keyExists(self::CONFIG, $configName);
$extensions = [];
foreach (self::CONFIG[$configName] as $extension) {
$extensions[] = new $extension();
}
return $extensions;
}
}
It is called like this in the templates:
{{ markdown_content|custom_markdown }} {# to use the default markdown configuration #}
{{ markdown_content|custom_markdown('inline') }} {# to remove all paragraph tags from the result #}
I'm designing a simple templating system for a CMS in PHP which internally currently uses something like:
require_once 'templates/template1.php`;
to import the desired template.
I would like every content {{field123}} in this PHP file to be automatically converted into <?php echo $row['field123']; ?> before being passed into require_once and executed by PHP.
Is there a way to activate a preprocessor (I know that PHP is already named after preprocessor) that does this replacement {{anything}} -> <?php echo $row['anything']; ?> before executing the PHP code template1.php? If not, what's the usual way to do this?
Having PHP code in templates - especially code with potential side-effects - can get dirty real quick. I would recommend using static templates, treating them as strings instead of executing them, then parsing them for tokens, with your main application compiling them and handling output.
Here is a rudimentary implementation that parses variables into tokens, and also handles mapped function calls in your templates. First, "fetching" our template (for a simple example):
$tpl = 'This is a sample template file.
It can have values like {{foo}} and {{bar}}.
It can also invoke mapped functions:
{{func:hello}} or {{func:world}}.
Hello user {{username}}. Have a good day!';
Then, the template parser:
function parse_template(string $tpl, array $vars): string {
// Catch function tokens, handle if handler exists:
$tpl = preg_replace_callback('~{{func:([a-z_]+)}}~', function($match) {
$func = 'handler_' . $match[1];
if(function_exists($func)) {
return $func();
}
return "!!!What is: {$match[1]}!!!";
}, $tpl);
// Generate tokens for your variable keys;
$keys = array_map(fn($key) => '{{' . $key . '}}', array_keys($vars));
// Substitute tokens:
$tpl = str_replace($keys, $vars, $tpl);
return $tpl;
}
These are our handler functions, with handler_X matching {{func:X}}.
function handler_hello() {
return 'HELLO THERE';
}
function handler_world() {
return '#Current World Population: ' . mt_rand();
}
Then, here are the variables you'd like to parse in:
$vars = [
'foo' => 'Food',
'bar' => 'Barnacle',
'username' => 'Herbert'
];
Now let's parse our template:
$parsed = parse_template($tpl, $vars);
echo $parsed;
This results in:
This is a sample template file.
It can have values like Food and Barnacle.
It can also invoke mapped functions:
HELLO THERE or #Current World Population: 1477098027.
Hello user Herbert. Have a good day!
Job done. You really don't need a complicated templating engine for something like this. You could easily extend this to allow the handlers to receive arguments defined in the template tokens -- however I'll leave that for your homework part. This should do to demonstrate the concept.
As mentioned in a comment and in How do I capture PHP output into a variable?, the use of output buffering can work:
<?php
ob_start();
?>
Hello
{{field123}} and {{field4}}
World
<?php // or require_once 'template1.php'; ?>
<?php
$s = ob_get_clean();
$a = array('field123' => 'test', 'field4' => 'test2');
$s = preg_replace_callback('/{{(.*?)}}/', function ($m) use ($a) { return isset($a[$m[1]]) ? $a[$m[1]] : $m[0]; }, $s);
echo $s;
?>
// Output:
// Hello
// test and test2
// World
Here we also used a method similar to Replace with dynamic variable in preg_replace to do the replacement.
I was wandering if it were possible to store a html schema page with special strings to replace with variable and how to do it.
In an external file, I would like to put the html structure of a product, let's call it schema.php:
<span id="{% id %}">{%= name %}</span>
<span>{%= imageURL() %}</span>
The example above is just a simpler example. In the external file, the html would be more complex. I know that if there were just few lines I could just echo them with a simple function but this is not the case.
In another file I have a class that handle products, let's call it class.php:
class Product {
//logic that is useless to post here.
public function imageURL() {
return "/some/url".$this->id."jpg";
}
}
In this class I would like to add a function that take the content from schema.php and then echo it in the public file for users.
I tried with file_get_contents() and file_put_contents() but it just doesn't work:
$path_to_file = 'data/prodotti/scheda.inc';
$file_contents = file_get_contents($path_to_file);
$file_contents = str_replace(
"{%= ",
"<?php echo $this->",
$file_contents
);
$file_contents = str_replace(
" }",
"; ?>",
$file_contents
);
file_put_contents($path_to_file, $file_contents);
is it possible to call schema.php page and print it with custom variables?
By "schema page" I think you mean "template" and yes, but the best way to do it is to use an existing templating engine such as Smarty or a Mustache implementation like https://github.com/bobthecow/mustache.php instead of implementing it yourself because of the risks of XSS, HTML-injection, and how you'll eventually want features like looping and conditionals.
you can do it normaly with php require func. without any strings to replace, if you just want to use that file as "template" then:
in schema.php:
<?php
echo'<span id="'.$id.'">'.$name.'</span>
<span>'.$imageURL.'</span>';
?>
in class.php:
<?php
class Product {
//logic that is useless to post here.
public function imageURL() {
return "/some/url".$this->id."jpg";
}
}
$imageURL = imageURL(); ?>
Index.php or whatever the main page that handles class.php and temp.php(schema)
<?php
//avoid undefined variables on errors
//in case that you don't check for values submitted
$id = 0;
$name = 0;
$imageURL = '';
//set vars values
$id = /*something*/;
$name = /*something 2*/;
$imageURL = /*something3*/;
//all date will be replaced is ready, oky nothing to wait for
require('path/to/schema.php');
Note: If you gets these data from user, then you should validate with if(isset()).
hope that helps,
I am migrating from clean php views to smarty, and I have a problem converting the following template into smarty:
<?php
use \framework\core;
?>
<h1><?= Core::translate('some string to translate'); ?></h1>
So I want to use some class that's loaded via autoloader, and then use its translate method. How can I do that in smarty?
This is how I did this same task in one of my older projects using Smarty API extension:
// register additional smarty modifiers for I18N
$this->smarty->register_block("translate", array($this, "smarty_i18n_translate"));
// better alias
$this->smarty->register_block("_", array($this, "smarty_i18n_translate"));
// translation function
public function smarty_i18n_translate($params, $content, &$smarty, &$repeat)
{
if (isset($content))
{
if (isset($params['resourceID']))
{
$resourceID = $params['resourceID'];
unset($params['resourceID']);
}
else
$resourceID = NULL;
// setting context vars if specified
foreach ($params as $key => $val)
{
$this->setContextVar($key, $val);
}
// Core::translate($content); ?
return $this->translate($content, $resourceID);
}
return '';
}
This allows writing in views (I used {? as smarty tag delimiter):
<span class="label">{?_?}Operation type{?/_?}:</span>
One note: this was for some pretty ancient version of Smarty, they may have changed details of API, but it seems register methods are still there.
Btw in the docs there is this same problem illustrated as example: http://www.smarty.net/docsv2/en/api.register.block.tpl
try this logic/way, hope you might get some idea..
<?php
$my_obj = new MyClass();
$smarty->left_delimeter('{{');
$smarty->right_delimeter('}}');
$smarty->assign('my_obj', $my_obj);
$smarty->display('your.html');
?>
Then on your html
<h1>{{ $my_obj->translate('some string to translate'); }}</h1>
I have a CMS where users can create and edit their own content in their websites. I also provide the possibility to include forms and galleries by simply replacing specific Div's in their content.
In the past I simply exploded the content on these Div's to an array, replaced the whole Div's with the needed html code (by using PHP's include) to show the form or gallery at that exact position, imploded the whole array to a string again (html) and used in the website.
Now I am trying to achieve the same in Laravel 5:
// example plugins div in HTML
// ******************************
<div class="plugin form"></div>
// PageController.php
// ******************************
$page = Page::where(('url', '=', "home")->first();
$page->text = Helpers::getPlugins($page->text);
// Helpers.php (a non default custom class with functions)
// ******************************
public static function getPlugins($page)
{
$dom = new DOMDocument();
$dom->loadHTML($page, LIBXML_HTML_NOIMPLIED);
$x = $dom->getElementsByTagName("div");
foreach ($x as $node)
{
if (strstr($node->getAttribute('class'), "plugin"))
{
$plugin = explode(" ",$node->getAttribute('class'));
$filename = base_path() . "/resources/views/plugins/" . trim($plugin[1]) . ".blade.php";
if (is_file($filename))
{
ob_start();
include($filename);
ob_get_contents();
$node->nodeValue = ob_get_clean();
}
else
{
$node->nodeValue = "Plugin <strong>".$node->getAttribute('class')."</strong> Not found</div>";
}
}
}
return $dom->saveHTML();
}
Sofar so good, the content is returned but what I get is all the pure text blade markup instead of the Laravel generated html which I want to use.
I think there is a way this could work but I cannot come to think of it.
Try manually building the template by using the method BladeCompiler->compile(), read more here
Edit: I think the facade Blade::compile() will give you access to this function too, just add use Blade at the top of the file.