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.
Related
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.
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 trying to find the cleanest way to merge multiple html files into one html file. This way I can easily change parts of the html or show them only on certain pages. The file list is as followed:
page.tpl (header, footer, head info)
sidebar.tpl (contains sidebar and sidebar blocks)
nav.tpl(contains navigation links in nested UL)
The page.tpl file looks like this:
<!DOCTYPE html>
<html>
<head>
<title>Page Title</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="author" content="Brandon" />
<meta name="robots" content="noindex,nofollow" />
<meta name="keywords" content="" />
<meta name="description" content="" />
<?php print $stylesheets; ?>
<?php print $scripts; ?>
</head>
<body>
<section id="wrapper">
<header>Header Title</header>
<nav><?print $nav; ?></nav>
<section><?php print $content; ?></section>
<aside> <?php print $sidebar; ?><aside>
<footer>© 2011 Brandon License: GPLv2</footer>
</section>
</body>
</html>
The main function I have to include everything is:
function theme($tpl, $vars = array()) {
extract($vars);
ob_start();
require($tpl);
$template = ob_get_contents();
ob_end_clean();
return $template;
}
$tpl is set to the page.tpl file.
I tried $vars['nav'] = file_get_contents('nav.tpl'); above the theme function just to give it some data to work with. If I remove the $tpl variable and the require() function, I see the UL nav list but when I add back the page.tpl file back in I get this error:
Warning: extract() expects parameter 1 to be array, null given
This works(shows UL nav list):
$vars['nav'] = file_get_contents('nav.tpl');
function theme($vars = array()) {
extract($vars);
ob_start();
$template = ob_get_contents();
ob_end_clean();
return $template;
}
This doesn't:
$vars['nav'] = file_get_contents('nav.html');
theme('page.html', $vars) //page.html is set to correct directory.
function theme($tpl, $vars = array()) {
extract($vars);
ob_start();
require($tpl);
$template = ob_get_contents();
ob_end_clean();
return $template;
}
Any help on getting this to work correctly would be appreciated.
UPDATE: This is my current index.php file:
<?php
define('ROOT_DIR', getcwd());
require_once(ROOT_DIR . '/core/includes/boot.inc');
boot_start(BOOT_FULL);
// Based off of Drupal's drupal_bootstrap(). Set's ini_set's, database
//and starts sessions. This works just fine and I haven't coded any
//theme/template code into it. The only thing boot_start() does for theme is
//load the .inc file that has the theme() function. The .inc gets included
// otherwise I would have gotten a "call to unknown function" error.
$vars['nav'] = file_get_contents(ROOT_DIR . '/core/templates/nav.tpl');
theme('./core/templates/page.tpl', $vars);
I don't quite understand why I am getting the error from extract(). When I add $vars['nav'] without including 'include($tpl)', extract works just fine. It isn't until I try to include the page.tpl file.
The page.tpl file should be loaded on every page request that outputs anything. So I think I only need theme($vars) instead of theme($tpl, $vars = array())
Is there a way I can include page.tpl without passing it to theme(), while passing $vars so that $vars['nav'] overrides the <?php print $nav; ?> tag in page.tpl? Thanks.
SOLVED: Man, I can't believe it took me this long to fix this. Since theme() returned and not echo'ed the data, I had to assign $theme = theme('page.tpl', $vars); then echo $theme; Besides a few PHP notices, it works.
I personally just like to make a file for the individual parts. and then include them.
<?php include('relative/link.php'); ?>
if you want to edit the content in a section I would use variables.
header.php
echo $foo;
index.php
$foo='bar';
include('header.php');
when we include a file it grabs the contents and injects it in the current file and then will process it.
This is more of a style question. I have a template file header.php in which I define a PrintHeader() function.
Callers of this function can specify, via global variables, the title of the page and any Javascript scripts to include when printing the header (because surely not every page will have the same title or want to include the same scripts). I chose to use global variables rather than function arguments because the latter would require the interface to change when adding new arguments.
Is this considered "good" style, and is there a "better" way to do what I'm trying to do?
header.php (simplified)
<?php
function PrintHeader()
{
global $pageTitle, $scripts; // Set by the caller of this function
echo <<<HEADER
<html>
<head>
<title>$pageTitle</title>
HEADER;
if( !empty($scripts) )
{
foreach($scripts as $script)
{
echo " <script type=\"text/javascript\" src=\"$script.js\"></script>\n";
}
}
echo " </head>\n";
}
?>
index.php (simplified)
<?php
$pageTitle = 'Welcome';
$scripts = array('script1', 'script2');
require('header.php');
PrintHeader();
// Print the rest of the page
?>
is there a "better" way to do what I'm trying to do?
sure.
I see no point in defining and calling a function at all. as well as in using heredoc.
header.php (dramatically simplified):
<html>
<head>
<title><?=$pageTitle?></title>
<? if( !empty($scripts) ): ?>
<? foreach($scripts as $script): ?>
<script type="text/javascript" src="<?=$script?>.js"></script>
<? endforeach ?>
<? endif ?>
</head>
index.php:
<?php
$pageTitle = 'Welcome';
$scripts = array('script1', 'script2');
require('header.php');
?>
but still it's not the best way, as it seems you're not using a template where it most valuable - to output page contents itself.
So, I'd make it in three parts:
links.php (simplified):
<?
//include our settings, connect to database etc.
include dirname($_SERVER['DOCUMENT_ROOT']).'/cfg/settings.php';
//getting required data
$DATA = getdata("SELECT * FROM links");
$pagetitle = "Links to friend sites";
//etc
//and then call a template:
$tpl = "links.tpl.php";
include "main.tpl.php";
?>
where main.tpl.php is your main site template, including common parts, like header, footer, menu etc:
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>My site. <?=$pagetitle?></title>
</head>
<body>
<div id="page">
<? include $tpl ?>
</div>
</body>
</html>
and finally links.tpl.php is the actual page template:
<h2><?=$pagetitle?></h2>
<ul>
<? foreach($DATA as $row): ?>
<li><?=$row['name']?></li>
<? endforeach ?>
<ul>
notice native HTML syntax, which is highlighted, readable and centralized in one place instead of being split between numerous functions and files
The point is in having separate template for the every PHP page as well as main site template for them all. With such setup you'll get a lot of advantages such as custom error pages, multiple representations of the same data (say, HTML, JSON or XML) by switching only templates without changing the code and many more
The use of global variables is certainly not advisable, and I question the necessity of using heredoc as you have - not that there is anything inherently wrong with heredoc, just that you seem to have rather arbitrarily utilized it in this sample template.
It is not elegant to use a return-value of a function as the output of each template - this defeats one of the purposes of templates which is re-usability.
Take a look at smarty, if not to directly use it (after all, why re-invent the wheel), at least to get an idea of how a rendering class is used to shuttle in the variables that a template needs without resorting to messy globals.
Here's a very quick overview of a way to do templating:
You have a template class that you can assign data to and then render a template.
Template.php:
class Template
{
protected $data = array();
public function assign($key, $value)
{
$this->data[$key] = $value;
}
public function render($file)
{
extract($this->data);
require $file;
}
}
You then have your template, header.php:
<html>
<head>
<title><?php echo $pageTitle; ?></title>
....
In index.php, you then use the template class to assign data and render your template.
$tpl = new Template;
$tpl->assign('pageTitle', 'My page title!');
$tpl->render('header.php');
This is just a simple example to demonstrate the idea, and could give you a good starting point.
While "better" may be in the eye of the beholder, I would suggest having some sort of functions that set the page bits rather than exposing raw variables. For instance, instead of doing $pageTitle = 'Welcome'; you could have set_page_title('Welcome');.
For JavaScript you could have a function that adds to the current script set -- rather than possibly replacing it all -- such as add_javascript($code);. This will allow a developer to set all of these without having to keep track of what the variable name was, and also without needing to global it as well if they want to set it from within a function.
This is an alternative using output buffering.
p/example_page/index.php is one of your pages:
<?php
ob_start() ?>
<h1>Example</h1>
<p>This is the page content</p>
<?php $main = ob_get_clean();
ob_start() ?>
<script defer src="js/example_page/example.js"></script>
<?php $script = ob_get_clean();
$title = 'Example page';
include 'templates/base.php';
templates/base.php is your reusable layout:
<!doctype html>
<html>
<head>
<script defer src="js/main.js"></script>
<?php echo $script ?>
<title><?php echo $title ?> - Example website</title>
</head>
<body>
<header>
<nav aria-label="Main menu"></nav>
</header>
<main><?php
echo $main;
?></main>
<footer>Example footer</footer>
</body>
</html>
Global variables are generally considered bad, and should be avoided if possible.
Rather than listing every variable in the interface, as you said things could change, pass a single array to the PrintHeader() functions:
<?php
function PrintHeader($opts=array()) {
if(!isset($opts['title'])) $opts['title'] = 'Default Title';
echo <<<HEADER
<html>
<head>
<title>$opts['title']</title>
HEADER;
if(!empty($opts['scripts'])) {
foreach($opts['scripts'] as $script) {
echo " <script type=\"text/javascript\" src=\"$script.js\"></script>\n";
}
}
echo " </head>\n";
}
$opts = array('title'=>'Welcome',
'scripts'=>array('script1', 'script2'));
require('header.php');
PrintHeader($opts);
?>
This way, you can add new capabilities in the function without breaking old code.
I am working on my own MVC framework. Below is an example controller I have so far.
I have a way of loading models into my controller and also view files.
I am wanting to also have different template options for my site. My template will just be a page layout that inserts the views that are created from my controller into the middle of my template file.
/**
* 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 and pass the Model data into it
$this->view->load('userProfile', $profileData);
}
}
Here is a basic idea of the template file...
DefaultLayout.php
<!doctype html>
<html lang="en">
<head>
</head>
<body>
Is the controller has data set for the sidebar variable, then we will load the sidebar and the content
<?php if( ! empty($sidebar)) { ?>
<?php print $content; ?>
<?php print $sidebar; ?>
If no sidebar is set, then we will just load the content
<?php } else { ?>
<?php print $content; ?>
<?php } ?>
</body>
</html>
Another Template without any header, footer, anything else, can be used for AJAX calls
EmptyLayout.php
<?php
$content
?>
I am looking for ideas on how I can load my main template file and then include and view files into the content area of my main layout file?
In the sample layout file, you can see that the content area has a variable called $content. I am not sure how I can populate that with the views content, to be inserted into my main layout template. If you have any ideas, please post sample
Something a little bit like
function loadView ($strViewPath, $arrayOfData)
{
// This makes $arrayOfData['content'] turn into $content
extract($arrayOfData);
// Require the file
ob_start();
require($strViewPath);
// Return the string
$strView = ob_get_contents();
ob_end_clean();
return $strView;
}
Then use with
$sidebarView = loadView('sidebar.php', array('stuff' => 'for', 'sidebar' => 'only');
$mainView = loadView('main.php', array('content' => 'hello',, 'sidebar' => $sidebarView);