My set-up comprises a lib folder with classes and a view folder with PHP files, that produce output. The views are imported inside a View class similar to this:
class View {
public function render(string $basename, Array $params) : string {
extract($params, EXTR_PREFIX_INVALID, 'v');
ob_start();
include sprintf('%s/views/%s.php', dirname(__DIR__), $basename);
$out = ob_get_contents();
ob_end_clean();
return $out;
}
}
I have basically two problems with Psalm in this situation:
For View::render it reports a UnresolvableInclude. I can even type the $basename with something like
#param "view1"|"view2"|"about" $basename
without effect. The unresolvable include remains.
The extract() puts the content of $params in the local scope, where the view files are included. This allows me to have
<?=escape($foo)?>
“tags” in my view files with $params === ['foo' => 'bar']. However, Psalm doesn’t catch up on this and reports a lot of UndefinedGlobalVariable problems.
My question: How can I tell psalm about the view files and the variables? Or alternatively, how can I re-structure this code so that psalm can test it for me?
There's a demo TemlateChecker plugin in Psalm's repo that seems to do something similar: it looks at the docblock in the view file for the tag like #variablesfrom ClassName::method and makes them available in the template file. Or just properties on $this variable from that method, not sure. It's also mentioned in Psalm docs: Checking non-PHP files.
Alternatively, you could wrap your template into a minimal method/function as technically view is just a function that takes a bunch of variables and returns a string: https://psalm.dev/r/66898ee87f
<?php class HomePageView { // view starts here
/** #param list<string> $sections */
public function render(
string $title,
array $sections
): string { ob_start();
?>
<html>
<head>
<title><?=$title?></title>
</head>
<body>
<?php foreach ($sections as $section): ?>
<section><?=$section?></section>
<?php endforeach; ?>
</body>
</html>
<?php return ob_get_contents(); }} // view ends here ?>
This way any tool that analyzes code (including Psalm, but not limited to) would be able to understand it.
Related
i want to create a template engine with php regex
my template engine is the following code
<?php
class tmp
{
public function assign($name,$val){
$GLOBALS[$name]=$val;
}
public function compile($buffer){
$buffer= '?> '.preg_replace('~\{\!\!\s*\$(\w+)\s*\!\!\}~', '<?php echo $GLOBALS["$1"]; ?>', $buffer);
file_put_contents('compiled.php', $buffer);
return $buffer;
}
public function run($run){
return eval($run);
}
}
?>
and my theme is the following code
{!! $bar !!}
and compiled theme is
?>
<?php echo $GLOBALS["bar"]; ?>
I access to vars with $GLOBALS
but in blade the compiled code is as follows
<?php echo $bar; ?>
how blade can access to defined vars directly with their name?
I can't confirm this is how blade is doing it, though I suspect it would be, but basically extract does that exactly what you are asking:
http://php.net/manual/en/function.extract.php
It takes an array of variables and inserts directly into the symbol tables so they be accessed directly. Used in combination with get_defined_variables() it becomes very useful.
I usually use Smarty template engine, so i separate database quesries and other logic from HTML template files, then assign received in PHP variable into Smarty via their function $smarty->assign('variableName', 'variableValue'); then display correct template file with HTML markup, and then i can use within that template my assigned variables.
But how correctly it will be done with .php file tempaltes, without Smarty?
For example, i use that construction:
_handlers/Handler_Show.php
$arData = $db->getAll('SELECT .....');
include_once '_template/home.php';
_template/home.php
<!DOCTYPE html>
<html>
<head>
....
</head>
<body>
...
<?php foreach($arData as $item) { ?>
<h2><?=$item['title']?></h2>
<?php } ?>
...
</body>
</html>
It's work. But i heard that it's not the best idea to do that.
So is this approach correct? Or maybe there's other way to organize it?
Give me advice, pelase.
Including templates in such a manner like in your example is not best idea because of template code is executed in the same namespace in which it is included. In your case template has access to database connection and other variables which should be separated from view.
In order to avoid this you can create class Template:
Template.php
<?php
class Template
{
private $tplPath;
private $tplData = array();
public function __construct($tplPath)
{
$this->tplPath = $tplPath;
}
public function __set($varName, $value)
{
$this->tplData[$varName] = $value;
}
public function render()
{
extract($this->tplData);
ob_start();
require($this->tplPath);
return ob_get_clean();
}
}
_handlers/Handler_Show.php
<?php
// some code, including Template class file, connecting to db etc..
$tpl = new Template('_template/home.php');
$tpl->arData = $db->getAll('SELECT .....');
echo $tpl->render();
_template/home.php
<?php
<!DOCTYPE html>
<html>
<head>
....
</head>
<body>
...
<?php foreach($arData as $item): ?>
<h2><?=$item['title']?></h2>
<?php endforeach; ?>
...
</body>
</html>
As of now template hasn't access to global namespace. Of course it is still possible to use global keyword,
or access template object private data (using $this variable), but this is much better solution than
including templates directly.
You can look at existing template system source code, for example plates.
I'm using PHP for web development. I'm using the following function to wrap the include of a view:
<?php
function render($templateFile) {
$templateDir = 'views/';
if (file_exists($templateDir . $templateFile)) {
include $templateDir . $templateFile;
} else {
throw new Exception("Template '{$templateFile}' couldn't be found " .
"in '{$templateDir}'");
}
}
?>
Although this seems right to me, there is a really unexpected behavior: when I define a variable to something (e.g. an array) and use render for including a view that uses that variable, I get an undefined variable error. But when I explicitely use include there is no error at all and things are just fine.
This is the script that calls render:
<?php
include 'lib/render.php'; // Includes the function above.
$names = array('Trevor', 'Michael', 'Franklin');
render('names.html'); // Error, but "include 'views/names.html'" works fine.
?>
And this is the file that uses the $names variable:
<html>
<head>
<title>Names</title>
</head>
<body>
<ol>
<?php foreach ($names as $name): ?>
<li><?php echo $name; ?></li>
<?php endforeach; ?>
</ol>
</body>
</html>
Help will be very much appreciated.
This is from the PHP documentation on the include function (c.f. http://us1.php.net/manual/en/function.include.php):
When a file is included, the code it contains inherits the variable
scope of the line on which the include occurs. Any variables available
at that line in the calling file will be available within the called
file, from that point forward. However, all functions and classes
defined in the included file have the global scope.
And also:
If the include occurs inside a function within the calling file, then
all of the code contained in the called file will behave as though it
had been defined inside that function. So, it will follow the variable
scope of that function.
So, if your render function can't access $names, then neither can your included file.
A possible solution would be to pass the parameters you want to be able to access in your view template, to your render function. So, something like this:
function render($templateFile, $params=array()) {
$templateDir = 'views/';
if (file_exists($templateDir . $templateFile)) {
include $templateDir . $templateFile;
} else {
throw new Exception("Template '{$templateFile}' couldn't be found " .
"in '{$templateDir}'");
}
}
Then, pass them like this:
$names = array('Trevor', 'Michael', 'Franklin');
render('names.html', array("names" => $names));
And use them in your view template like this:
<html>
<head>
<title>Names</title>
</head>
<body>
<ol>
<?php foreach ($params['names'] as $name): ?>
<li><?php echo $name; ?></li>
<?php endforeach; ?>
</ol>
</body>
</html>
There are probably better solutions to this, like putting your render function into a View class. Then you can call the View class function from inside your template file, and access parameters that way instead of just assuming there will be a $params variable in the view templates scope. But, this is the simplest solution.
The problem is, when you include the file directly using include 'views/names.html' the variable $name remains in the same files scope. Hence, it works. But when the include is done through the function, the varibale $name remains out of scope inside the function. So it doesn't work. For example, declare $names as global inside the function and it will work.
If you update the function like below you will see $names variable works.
function render($templateFile) {
global $names; // declares the global $names variable to use in the included files
$templateDir = 'views/';
if (file_exists($templateDir . $templateFile)) {
include $templateDir . $templateFile;
} else {
throw new Exception("Template '{$templateFile}' couldn't be found " .
"in '{$templateDir}'");
}
}
I am working on a simple template engine, and I was wondering if it's feasible to include the template file multiple times, once for each time the template is rendered. It basically goes like this:
function rendering_function_in_rendering_class()
{
include $path_to_templates . get_class($this) . 'template.php';
}
And then in the template file:
<h1>Hello, <?php echo $this->awesomename ?>!</h1>
This function does exactly what you need:
<?php
function embed($file, $vars) {
ob_start();
extract($vars, EXTR_SKIP);
include($file);
$content = ob_get_contents();
ob_end_clean();
return $content;
}
?>
It takes file path as a first parameter and key/value array of variables which will be extract into the scope such that your template will be able to use them directly in HTML like this:
<h1><?php print $title; ?></h1>
No, functions in PHP may only be defined once. However, you can make more than one instance of each class:
$this1=new rendering();
$this2=new rendering();
echo $this1->awesomename;
echo $this2->awesomename;
Or use a function instide a class without the class being initialized:
$rendering::rendering_function_in_rendering_class();
Does that answer your question?
Question Updated
I am building an MVC framework, for my templates and views, I will have a main page template file and my views will be included into this template.
The only way I have seen to do this is to use output buffereing
ob_start();
include 'userProfile.php';
$content = ob_get_clean();
Is there any other way of doing this? I think output buffering is not the best on performance as it uses a lot of memory
Here is a sample controller, the $this->view->load('userProfile', $profileData);
is the part that will be loaded using output biffering so that it can be included into the main template below into the $content part
view class
public function load($view,$data = null) {
if($data) {
$this->data = $data;
extract($data);
} elseif($this->data != null) {
extract($this->data);
}
ob_start();
require(APP_PATH . "Views/$view.php");
$content = ob_get_clean();
}
controller
/**
* Example Controller
*/
class User_Controller extends Core_Controller {
// domain.com/user/id-53463463
function profile($userId)
{
// load a Model
$this->loadModel('profile');
//GET data from a Model
$profileData = $this->profile_model->getProfile($userId);
// load view file
$this->view->load('userProfile', $profileData);
}
}
main site template
<html>
<head>
</head>
<body>
<?php echo $content; ?>
</body>
</html>
Using a template system is not necessarily tied to output buffering. There are a couple of things in the example code you give that should certainly not be taken for granted:
One:
flushblocks(); // what does this do??
And two:
$s = ob_get_clean();
Why does the code capture the template output into a variable? Is it necessary to do some processing on this before outputting it? If not, you could simply lose the output buffering calls and let the output be sent to the browser immediately.