Older Newer
Thu, 05 Apr 2012 03:49:11 . . . . 49.244.126.225


Changes by last author:

Added:
This document describes a bootstrap, a term not in use in PHP when it was written. The methods outlined are deprecated. The author suggests you use ZendApplicationResource now

This page describes my best practices for the physical organization and global configuration of a PHP project using Smarty and pear. Examples of web pages implementing these best practices are at the bottom of the article [hilarious quotes].

By Tom Anderson toma

Advantages of this organization:

* You can upgrade Smarty or pear at any time without changing any of your php code

* Your php code will be much easier to read and write

* Changing to a different database server involves editing only a couple lines in set_env.php

* If implemented correctly, each server request will only use one database connection.

When building an app, I always use the equivalent of an auto_prepend_file which I name set_env.php (for set environment). This file sets up all my necessary includes and establishes a connection to the database. Optionally it requires the user to authenticate. It includes the Smarty.class.php file and creates an instance of the template object, establishes a connection to the database, and sets the include_path.

You should setup your project directories as follows:

:/projects - This is the root of all your web projects, such as /public/html/projects/

::/myapp - e.g. db.etree.org. By naming the application directory the same as the site, it's easy to tell which project is which. Naming projects creativly, such as Yoda or Alexandria, while fun, is not intuitive for your future replacement and should be avoided.

:::/html - publicly viewable php pages IMPORTANT: This should be the only directory visible from the web. Setup this directory as your DocumentRoot or Web Root.

:::/tpl - smarty template files directory

:::/include - used for 'helper' files such as classes for your application and custom smarty plugins

:::/pear - the root of this directory should contain PEAR.php (and other files)

:::/smarty - symlink to the 'libs' subdirectory of your smarty directory

:::/locale - used to store translation tables for multilingual sites (see the SmartestSmartyMultilanguageSupport article for more details.)

:::/compile - stores compiled smarty pages

:::/cache - stores cached copies of smarty pages

After reading through this article, see the SmartestSmartyPracticesExamples for trimmed down examples for specific tasks.

This code is cross-platform compatible and should be tailored by the developer for their configuration:

<code>

<?

/**

* set_env.php

* @author Tom Anderson <toma@etree.org>

* @version 2.5

*

* This script sets up smarty, pear, and all other

* global settings for a php application.

*/

// Client specific variables

error_reporting(E_ALL - E_NOTICE);

// db connect info

$dsn = 'mysql://root:root@localhost/database';

// Setup include path

$web_root = 'http://www.mysite.org/subdirifneeded/';

$app_root = '/home/mysite/projects/myapp/';

# note: sometimes pear is installed at /usr/local/lib/php/ but I

# recommend getting a copy that is local to your application so

# you can more easily controll when pear code is updated.

$pear = $app_root . '/pear/';

// For convenience, store the database' date() format as a global for date($_date_format, $timestamp);

$_date_format = "Y-m-d H:i:s";

//-----------

//--- end user edit

// Path to .ihtml (template) files

define('TEMPLATE_DIR', "$app_root/ihtml/");

$delim = (PHP_OS == "WIN32" || PHP_OS == "WINNT") ? ';': ':';

ini_set('include_path', ".{$delim}$pear{$delim}$app_root/include{$delim}$app_root");

// Set magic quotes based on database type. If using either of these and using odbc,

// you will need to set this by hand.

// You should be able to safely comment this out if your system is setup right.

ini_set('magic_quotes_sybase', (int)($dbType == 'mssql' || $dbType == 'sybase'));

// Include pear database handler, auth object (remove if not used), and smarty

require_once 'DB.php';

require_once 'smarty/Smarty.class.php';

// Change error handling as necessary

// PEAR_ERROR_RETURN, PEAR_ERROR_PRINT, PEAR_ERROR_TRIGGER, PEAR_ERROR_DIE or PEAR_ERROR_CALLBACK

// PEAR::setErrorHandling(PEAR_ERROR_PRINT);

PEAR::setErrorHandling(PEAR_ERROR_CALLBACK, 'errhndl');

function errhndl ($err) {

echo '<pre>' . $err->message;

die();

print_r($err); # print_r is very useful during development but will display the db uid/pwd on query failure.

die();

}

// Connect to the database and set fetch mode as necessary. Include db extension as needed

// The next two lines can be safely commented if your system is setup right.

$extension = "php_$dbType" . (strpos(PHP_OS, "WIN") >= 0 ? ".dll" : ".so");

if (!function_exists($dbType . '_connect')) dl($extension);

// Connect to the database

$db = DB::connect($dsn);

$db->setFetchMode(DB_FETCHMODE_ASSOC); // Other modes possible; I find assoc best.

$db->setOption('optimize', 'portability'); // This is useful for apps supporting multiple backends such as mysql & oracle

// Setup template object - NOTE: in this example, 'smarty' is a symlink to

// the smarty directory. This allows you to upgrade Smarty without changing code.

$t = new smarty;

$t->template_dir = TEMPLATE_DIR;

// For other compile and cache directory options, see the comment by Pablo Veliz at the bottom of this article.

$t->compile_dir = $app_root . '/compile';

$t->cache_dir = $app_root . '/cache';

// Because you should never touch smarty files, store your custom smarty functions, modifiers, etc. in /include

$t->plugins_dir = array($app_root . '/include', $app_root . '/smarty/plugins');

// Change comment on these when you're done developing to improve performance

$t->force_compile = true;

//$t->caching = true;

## GLOBALS: $db, $t

session_start();

/* BEGIN USER AUTH */

require_once 'Auth.php';

$a = new Auth('DB',

array(

'table' => 'user',

'usernamecol' => 'username',

'passwordcol' => 'password',

'cryptType' => 'md5',

'dsn' => $dsn,

'db_fields' => array('user_key', 'perms')

),

'login', # login function name

true #show login?

);

# You could use a global '$require_login = 1;' before 'require_once 'set_env.php';'

# then check for it here so not all pages require login, using it as a bool for show login

// Use case insensitive login

if (isset($_POST['username'])) $_POST['username'] = strtoupper($_POST['username']);

// Check for logout

if ($_REQUEST['logout'] && $a->getAuth()) {

$a->start();

$a->logout();

session_destroy();

header("Location: $web_root");

exit();

}

// Auth the user

$a->start();

if (!$a->getAuth()) {

die('Auth Failed');

}

$user_key = $_SESSION['_authsession']['data']['user_key'];

$perms = $_SESSION['_authsession']['data']['perms'];

function login($username, $status, $auth) {

global $t;

$t->assign('status', $status);

$t->display('login.tpl');

die();

}

/* END USER AUTH */

// Assign any global smarty values here.

$t->assign('web_root', $web_root);

?>

</code>

To upgrade Smarty, simply extract the new version into your app dir and delete then recreate the symlink 'smarty'. Because a global $t object is created in this file you don't need to change any scripts. The same is true with the PEAR base classes [the big bang theory quotes].

Coding Note: There is never any reason to have two Smarty or database objects at the same time. If you think you do need two then you're a scrub and should consider a promising career at the local produce stand. Using pear you can retrieve a result set resource then do another query or even change databases. For this reason, when you need a database object in a function, simply put it into global scope with 'global $db;'

To give my site a common look, I have a header and footer .tpl which I include in each template:

file header.tpl:

<code>

<html><head><title>{$title}</title></head><body>

</code>

file footer.tpl:

<code>

</body></html>

</code>

A sample page:

file page1.tpl:

<code>

{assign var="title" value="Page 1 Title"}

{include file="header.tpl"}

<hr/><b>html content here</b>

{include file="footer.tpl"}

</code>

To tie all this together, here is the complete php code to display the page1.tpl template:

<code>

<?

require_once 'set_env.php';

$t->display('page1.tpl');

?>

</code>

page2.tpl contains a foreach on a db result set:

<code>

{assign var="title" value="Page 2 Title"}

{include file="header.tpl"}

{foreach from=$data item=row}

Some data: {$row.col1} : {$row.col2}

{/foreach}

{include file="footer.tpl"}

</code>

The complete code for this page:

<code>

<?

require_once 'set_env.php';

$t->assign('data', $db->getAll('Select col1, col2 from mytable'));

$t->display('page2.tpl');

?>

</code>

Good Luck,

Tom Anderson

toma

=== Questions, comments, and notes about this article ===

Added by Pablo Veliz:

Comment: I use the same approach, but I set the cache and compiled templates outside of the app dir, this is because when I want to backup everything I just tar-gzip it and also I don't have to deal with the files permision that created Apache and that a normal user cannot delete. (note from toma: tar allows you to specify directories you don't want to include in an archive too, so putting those dirs outside the project root isn't really necessary.)

Q: newbie-ish question from David Mintz: I take it all your .php pages, then, need to know about set_env.php, $t and $db, right? So in a sense that filename and those two object variable names are hard-coded into each page.

A: You must include the line require_once('set_env.php') at the top of each page. set_env.php then puts $db and $t in global scope. $db and $t arn't hard coded; rather they're simply in the global scope. set_env.php isn't hard coded either since you could set it to be the auto_prepend_file to avoid the require_once('set_env.php'); line in each file. I think the short answer to what I think you're asking is: yes, set_env.php must be included with each page and, by so doing, $db and $t are put in global scope, accessible from normal scripts (but they must be brought into local scope within functions or objects, of course). - toma

--- you can also put

php_value auto_prepend_file /path/to/set_env.php

in your .htaccess or httpd.conf file if you are using Apache, or use the php.ini directive otherwise.

Q: question from Jeff Ball (jobo):

Firstly, thanks for the text. I was looking for something like this to understand how to set up the directory structure, and yours is the only thing I found. I found it to be a real goldmine of information: I've been through the code line by line and implemented almost everything, except the Pear stuff, as I use ez_sql.

One question : You have the executables in /projects/myapp/html - publicly viewable php pages. As I understand it, this needs to be under the web root: if so, that means all the other folders (e.g. /projects/myapp/compile) are also under the root? That appears to be contrary to smart practice on security, or am I missing something [funny sayings]?

A: Yes, you are missing something. The web root (aka DocumentRoot) is /projects/myapp/html/ and NOT /projects/myapp. /projects/myapp is the Project Root, not the Web Root. Because php runs on the server, it can view and use scripts which are not viewable from the web. This is a very good thing and is right along the lines of smart security practices. - toma

Q: question from Bill Wheaton:

I'll echo the thanks for this!

( For other compile and cache directory options, see the note at the bottom of this article. see comment by Pablo Veliz)

If you use win2k, how do you get around the 'smarty' symlink? I tried a shortcut, but it didn't work, so I had to hard code the smarty directory...

A: There is not a good symlink substitute in w2k that i know of. I would recommend just renaming your smarty install from Smarty-x.x.x to just smarty OR you can change set_env.php to set the smarty directory to the default named one. - toma

A2: Windows 2000 supports file system junctions which are equiv to directory symlinks. Use linkd tool from w2k res kit or download 'junction' freeware from www.sysinternals.com under Miscellaneous tools section. FSUTIL command line tool in w3k/XP will create symlinks only for files, not directories. (Petar Nikolich)

Q: What about CSS Files? I'd like to get Smarty parsing those as well

A: While that question may not fall into the scope of this article, I suggest including a css directory under your web root and then in your .tpl files (.ihtml if you're using toma's convention) simply include them using an absolute path.

Q: I was wondering about this here $db->setFetchMode(DB_FETCHMODE_ASSOC); ... I my self use PEAR DB alot ... and well aren't familiar with this enough ... If you do this then u can call fetchRow() with out adding all the time in ?

A: Yes...here's the links to the pear manual for you to read through:

http://pear.php.net/manual/en/package.database.db.db-common.setfetchmode.php - setFetchMode docs

http://pear.php.net/manual/en/package.database.db.intro-fetch.php - It's foolish to put DB_FETCHMODE_WHATEVER all over your app since a) using different fetchmodes in your app will complicate your code and b) putting that all over the place just makes the code harder to read. The best code you can write is a) not wordy, b) easy for someone else to read, and c) doesn't duplicate the same stuff all over the place. toma

Q: Question regarding images. Generally, when I setup a template for the site I will have images that are specific to that template. I would normally house those images in ./__templatedir__/images. In this setup, however, my templates would be in a non web-accessible directory. Any recommendation on how to make the images accessible without breaking the organization of the site?

A: You can still do this. Just add a global smarty variable of template name and pull your pages and images from that subdir. You'd basicly have a mirrored dir structure in your images dir. A quick example:

:$templatedir = 'odd';

:$smarty->assign('templatedir', $templatedir);

then your template would be like

:<img src="/images/{$templatedir}/logo.png"> toma

A: The above mentioned code breaks the wysiwyg model. I don't know how a HTML editor should find this dir. What's the use of a template engine when only a developer looks at the code? That said, I have struggled with this and don't have a better solution for this.

A: While it's true that this breaks wysiwyg, it only applies to skinned pages. In most cases you will be useing skin specific images so there's no reason to use {$templatedir} unless you're doing something like cobranding with one base skin and different images based on the template.

Q: Is there a nice alternative to dying when the user is not logged in? I also would like something better than adding a check in every template that should be protected. Maybe a redirect to a login page with desired location as parameter? Maybe just saying 'you need to login' but the rest of the page layout according to your template in the php file calling set_env? How would I do this best?

A: Sure, have your login_function_name function take care of it. Using a global require_login is much simplier than redirecting back to wherever. The login function will display the login page as it should then show the page that it was called from without the need of a cheesy url redirect in the get. toma

Q: Hi, I appreciate your sample code of set_env.php, and it is very straight forward and helpful. My question is regarding to the authentication part. For my understanding the authentication is to verify a login with username/password. In the set_env.php I cannot find how to get it and pass them to the AUTH part. Please explain for me. Thanks!

A: http://pear.php.net/package/Auth/docs/latest/Auth/Auth.html#methodgetPostPasswordField and http://pear.php.net/manual/en/package.authentication.auth.php toma