Best practices
This guide will give you solutions to common PHP design problems. It also provides a sketch of an application layout that I developed during the implementation of some projects.php.ini quirks
Some settings in the php.ini control how PHP interpretes your scripts. This can lead to unexpected behaviour when moving your application from development to the productive environment. The following measures reduce dependency of your code on php.ini settings.short_open_tag
Always use the long PHP tags:
<?php echo "hello world"; ?>
Do not use the echo shortcut
<?=
.asp_tags
Do not use ASP like tags:
<% echo "hello world"; %>
gpc_magic_quotes
I recommend that you include code in a global include file which is run before any $_GET or $_POST parameter or $_COOKIE is read. That code should check if the gpc_magic_quotes option is enabled and run all $_GET, $_POST and $_COOKIE values through the
stripslashes
function.register_globals
Never rely on this option beeing set. Always access all GET, POST and COOKIE values through the 'superglobal' $_GET, $_POST and $_COOKIE variables. For convenience declare
$PHP_SELF = $_SERVER['PHP_SELF'];
in your global include file after the gpc_magic_quotes quirk.File uploads:
The maximum size of an uploaded file is determined by the following parameters:
file_uploads
must be 1 (default)memory_limit
must be slightly larger than the post_max_size and upload_max_filesizepost_max_size
must be large enoughupload_max_filesize
must be large enough
Have one single configuration file
You should define all configuration parameters of your application in a single (include) file. This way you can easily exchange this file to reflect settings for your local development site, a test site and the customer's production environment. Common configuration parameters are:- database connection parameters
- email addresses
- options
- debug and logging output switches
- application constants
Keep an eye on the namespace
As PHP does not have a namespace facility like Java packages, you must be very careful when choosing names for your classes and functions.- Avoid functions outside classes whenever possible and feasible. Classes provide some extra namespace for the methods and variables that live inside them.
- If you declare global functions use a prefix. Some examples are dao_factory(), db_getConnection(), text_parseDate() etc.
Use a database abstraction layer
In PHP there are no database-independent functions for database access apart from ODBC (which nobody uses on Linux). You should not use the PHP database functions directly because this makes it expensive when the database product changes. Your customer may move from MySQL to Oracle one day or you will need an XML database maybe. You never know. Moreover an abstraction layer can ease development as the PHP database functions are not very userfriendly.Use Value Objects (VO)
VOs are actually a J2EE pattern. It can easily be implemented in PHP. A value object corresponds directly to a C struct. It's a class that contains only member variables and no methods other than convenience methods (usually none). A VO corresponds to a business object. A VO typically corresponds directly to a database table. Naming the VO member variables equal to the database fields is a good idea. Do not forget the ID column.class Person { var $id, $first_name, $last_name, $email; }
Use Data Access Objects (DAO)
DAO is actually a J2EE pattern. It can easily be implemented in PHP and helps greatly in separating database access from the rest of your code. The DAOs form a thin layer. The DAO layer can be 'stacked' which helps for instance if you want to add DB caching later when tuning your application. You should have one DAO class for every VO class. Naming conventions are a good practice.class PersonDAO { var $conn; function __construct(&$conn) { $this->conn =& $conn; } function save(&$vo) { if ($v->id == 0) { $this->insert($vo); } else { $this->update($vo); } } function get($id) { #execute select statement #create new vo and call getFromResult #return vo } function delete(&$vo) { #execute delete statement #set id on vo to 0 } #-- private functions function getFromResult(&vo, $result) { #fill vo from the database result set } function update(&$vo) { #execute update statement here } function insert(&$vo) { #generate id (from Oracle sequence or automatically) #insert record into db #set id on vo } }A DAO typically implements the following methods:
- save: inserts or updates a record
- get: fetches a record
- delete: removes a record
The DAO should only implement basic select / insert / update operations on one table. It must not contain the business logic. For example the PersonDAO should not contain code to send email to a person. For n-to-n relationships create a separate DAO (and even a VO if the relationships has additional properties) for the relation table.
Write a factory function that returns the proper DAO given the class name of a VO.Caching is a good idea here.
function dao_getDAO($vo_class) { $conn = db_conn('default'); #get a connection from the pool switch ($vo_class) { case "person": return new PersonDAO($conn); case "newsletter": return new NewsletterDAO($conn); ... } }
Generate code
99% of the code for your VOs and DAOs can be generated automatically from your database schema when you use some naming conventions for your tables and columns. Having a generator script ready saves you time when you are likely to change the database schema during development. I successfully used a perl script to generate my VOs and DAOs for a project. Unfortunately I am not allowed to post it here.Business logic
Business logic directly reflects the use cases. The business logic deals with VOs, modifies them according to the business requirements and uses DAOs to access the persistence layer. The business logic classes should provide means to retrieve information about errors that occurred.class NewsletterLogic { function __construct() { } function subscribePerson(&$person) { ... } function unsubscribePerson(&$person) { ... } function sendNewsletter(&$newsletter) { ... } }
Page logic (Controller)
When a page is called, the page controller is run before any output is made. The controller's job is to transform the HTTP request into business objects, then call the approriate logic and prepare the objects used to display the response.The page logic performs the following steps:
1. The
cmd
request parameter is evaluated. 2. Based on the action other request parameters are evaluated.
3. Value Objects (or a form object for more complex tasks) are created from the parameters.
4. The objects are validated and the result is stored in an error array.
5. The business logic is called with the Value Objects.
6. Return status (error codes) from the business logic is evaluated.
7. A redirect to another page is executed if necessary.
8. All data needed to display the page is collected and made available to the page as variables of the controller. Do not use global variables.
Note: it is a good idea to have a utility function that returns a parameter that is sent via GET or POST respectivly and provide a default value if the parameter is missing. The page logic is the only non-HTML include file in the actual page! The page logic file must include all other include files used by the logic (see base.inc.php below). Use the
require_once
PHP command to include non-HTML files.
class PageController { var $person; #$person is used by the HTML page var $errs; function __construct() { $action = Form::getParameter('cmd'); $this->person = new Person(); $this->errs = array(); if ($action == 'save') { $this->parseForm(); if (!this->validate()) return; NewsletterLogic::subscribe($this->person); header('Location: confirmation.php'); exit; } } function parseForm() { $this->person->name = Form::getParameter('name'); $this->person->birthdate = Util::parseDate(Form::getParameter('birthdate'); ... } function validate() { if ($this->person->name == '') $this->errs['name'] = FORM_MISSING; #FORM_MISSING is a constant ... return (sizeof($this->errs) == 0); } }
Presentation Layer
The top level page will contain the actual HTML code. You may include HTML parts that you reuse across pages like the navigation etc. The page expects the page logic to prepare all business objects that it needs. It's a good idea to document the business objects needed at the top of the page.The page accesses properties of those business objects and formats them into HTML.
<?php require_once('control/ctl_person.inc.php'); #the page controller $c =& new PageController(); ?> <html> <body> <form action="<?php echo htmlspecialchars($PHP_SELF) ?>" method="POST"> <input type="hidden" name="cmd" value="save"> <input type="text" name="name" value="<?php echo htmlspecialchars($c->person->name); ?>"> <button type="submit">Subscribe</button> </form> </body> </html>
Localization
Localization is a problem. You must choose amonga) duplicating pages
b) removing all hardcoded strings from your HTML.
As I work in a design company I usually take approach a). Approach b) is not feasible as it makes the HTML very hard to read and nearly impossible to edit in a visual web editor like Dreamweaver. Dynamic content is hard enough to edit with Dreamweaver. Removing also all strings, makes the page look quite empty...
So finish the project in one language first. The copy the HTML pages that need translation. Use a naming convention like
index_fr.php
to designate the French version of the index page. Always use the ISO two letter language codes. Do not invent your own language codes.To keep track of the language the user selected you must choose among
a) storing the language setting in a session variable or cookie
b) reading the preferred language (locale) from the HTTP headers the browser sends you
c) appending the language to the URL of every link in your application
While a) seems a lot more easier than c) it may be subject to session timeout. Option b) should only be implemented as an extension to a) or c).
Strings in a database must be localized too!
Making your application location independent
PHP has problems in some situations when include files are nested and reside in different folders and it is unclear at which directory level the file will be included. One can solve this by using absolute path names or using$_SERVER['DOCUMENT_ROOT']
as a starting point. However
this makes your code location dependent - it will not run anymore if you move it down your directory structure one level. Of cource we do not like
that.I have found a convenient solution to this problem. The toplevel page (the one that is called by the browser) needs to know the relative path to the application root directory. Unfortunately there is no such function in PHP and the webapp context concept is completely absent in PHP. So we can not automatically determine the application root reliably in all situations (It is *really* impossible. Don't even try. It's not worth the effort.)
Let's define a global variable called
$ROOT
in an include file in every directory that contains toplevel pages.
The include file (call it root.inc.php
) must be included by the page logic before any other include files.
Now you can use the $ROOT
variable to reference include files with their exact path!Sample:
We have toplevel pages in
/admin/pages/
. The $ROOT
variable must therefore be set to $ROOT = '../..';
.
The page logic included by pages in that folder would reference their include files like require_once("$ROOT/lib/base.inc.php");
.In my suggested folder outline (see below) we don't even need that, since all toplevel pages reside in the webapp root directory anyway. So the webapp root directory is always the current directory.
Folder outline
I suggest you make one file per class and follow a naming convention. Make sure that all your include files end with .php to avoid disclosure of your code to malicious users, which is a major security problem. I suggest the following folder structure:/ | Webapp root directory. Contains the pages that are actually called by the browser. |
/lib/ | Contains base.inc.php and config.inc.php |
/lib/common/ | Contains libraries and tools reusable for other projects, like your database abstraction classes. |
/lib/model/ | Contains the Value Object classes |
/lib/dao/ | Contains the DAO classes and the DAO factory |
/lib/logic/ | Contains the business logic classes |
/parts/ | Contains partial HTML that is included by pages |
/control/ | Contains the page logic. |
Provide a
base.inc.php
file that includes (require_once
) in the right order:
- frequently used stuff (database layer) from /lib/common
- the config include file
- all classes from /lib/model
- all classes from /lib/dao
Of course you will have additional directories for your images, uploaded files, ... etc.