LightRail is a light-weight web application framework for PHP. LightRail automates routine tasks and organizes your code base. It layers data, presentation and logic using the model/view/controller pattern (MVC) in a manner popularized by Ruby on Rails.
LightRail is:
There are many other frameworks for PHP employing MVC or a similar pattern and nearly all of them have a larger featureset than LightRail. It may very well be that you are better served by using one of them.
Unfortunately when frameworks attempt to be too comprehensive they can start turning into a 100 pound Swiss army knife — featureful but hardly ideal. LightRail handles a number of significant and repetitive aspects of your project and then lets PHP, itself a natural web application language, handle the rest. This is what makes it light.
You will notice that LightRail is a lot faster than other PHP frameworks, which is nice because good performance translates into being able to handle more site traffic volume.


Another benefit of using a light-weight framework is that it does not require a long learning process. PHP already handles a lot of web programming tasks very well on its own so why re-invent the wheel?
LightRail does not presume that you lack ideas of your own. The inheritance structure works with your add-ons and modifications.
LightRail is open source. You can modify it to suit your own needs and there is no spyware hiding in it.
This point was not entirely tongue in cheek. Hardware consumes power and creates disposal issues at the end of its life cycle. By getting more out of your hardware you reduce carbon emissions, toxic waste and operating costs.
Some of LightRail's features include:
LightRail 1.0.5 requires a server which supports PHP 5.1.4 or higher. If you want to use the integrated database features you will also need PDO support and a PDO compatible database.
While the documentation is Apache-centric in places, there is no reason LightRail will not work in other environments.
LightRail employs a model, view, controller pattern (MVC) similar to Ruby on Rails. MVC is a form of layered or tiered application development. By separating development into layers applications become more manageable, especially as they expand.
LightRail has classes corresponding to each of these components named LightRail_Record, LightRail_View and LightRail_Controller.
The model layer (LightRail_Record) handles object relational mapping (ORM). ORM converts database records to programming objects and vice-versa. If you don't need to interact with a database you don't need this feature.
The view is the presentation/interface layer, usually HTML, which is output to the user.
The controller is the principal logic layer which ties everything together.
As with most things in life, ASCII art will facilitate our understanding:
+-------------+
| View |
+-------------+
|
+-------------+
| Controller |
+-------------+
|
+-------------+
| Model |
+-------------+
|
+-------------+
| (Database) | <- now it all makes sense
+-------------+
LightRail classes make use of some of the magic methods available in PHP 5. If you want to define a magic method in one of your child classes you should ensure it does not interfere with LightRail. (unless that is what you are intending)
Example:
<?php
class DemoController extends LightRail_Controller
{
public function __get($name)
{
if ($name == 'sharkLaser') {
// This part is up to you
} else {
return parent::__get($name);
}
}
}
Character case is important in LightRail file and directory names. This is in contrast to the case flexibility in LightRail URLs.
The easiest way to install LightRail is to download a zipped copy and place the contents of the 'web' folder in your web directory.
Let's take a look at the contents.
The file 'index.php' is the site router. It handles all incoming requests and contains the site configuration.
The included directories are:
'application' contains the files for the application: the controllers, views, models, etc. It should not be browseable since none of the files in it handle direct requests.
The included '.htaccess' file will instruct most Apache servers not to serve content from the application directory. It does not hurt to test that access is denied when you browse to it. If your server does not support .htaccess configuration, secure the directory using the correct procedure.
Alternately, you can place the application directory outside the web viewable directory tree and point to it in your configuration.
The principle subdirectories of the application directory are:
'lib' contains the LightRail libraries and can optionally be used to contain other PHP libraries you like to use. Obviously, it should not be browseable.
The included '.htaccess' file will instruct most Apache servers not to serve content from the library directory. It does not hurt to test that access is denied when you browse to it. If your server does not support .htaccess configuration, secure the directory using the correct procedure.
Alternately, you can place the library directory outside the web viewable directory tree.
'public' is used to contain files which are to be served directly, e.g. things like images and CSS stylesheets. It should be browseable. You do not need to use this folder but if you do LightRail_View can simplify URL's to it.
LightRail is run with the static method LightRail::run() and configuration options are passed to it in a key/value array. Both the configuration and run call are contained in the router 'index.php' by default. Here is one example:
<?php
// Load LightRail
require('lib/lightrail-1.0.5/lightrail.php');
/////////////////////////////////////////////////
// Configuration
$conf = array();
// Use TestController::indexAction() as the site index
$conf['index'] = 'test/index';
/////////////////////////////////////////////////
// Run LightRail
LightRail::run($conf);
Alternately, if you prefer not to have configuration and/or logic in your web accessible directories you can just use 'index.php' to refer to a bootstrap file outside the web accessible directories and put the contents of 'index.php' into it.
<?php
// Load the bootstrap which contains the configuration and run call
require('../bootstrap.php');
Most of LightRail's default settings will work for a standard set up without needing to be set explicitly.
However, if you have more than one controller you will need to set the 'index' and if you are using the integrated database features you will also need to set the 'pdo' configurations.
If you have more than one controller you will need to set this value.
The controller and action method to use as the site index, specified in the form of an application URL. Because it is an application URL you may provide additional segments if desired.
Example: 'test/index' would invoke TestController::indexAction() as the site index.
Defaults to 'default/index'.
In LightRail all requests are handled by the file 'index.php' in the top level directory. All application URLs need to get directed to that file. Because it handles all application requests it is reffered to as the router file.
It is important that your server treats 'index.php' as the directory index. Most PHP enabled servers already do that but if yours does not you will need to set it. For Apache this can often be done with an .htaccess directive.
DirectoryIndex index.php
There are three ways application URLs can be directed to the router file:
LightRail can automate the generation of application URLs to suit the method you choose.
LightRail application URL arguments can be placed after the router file name and query string symbol: 'index.php?'. This is known as the 'urlPrefix' in LightRail's configuration.
http://example.com/index.php?/controller/action/arg
http://example.com/index.php/controller/action/arg
http://example.com/index/controller/action/arg
http://example.com/?/controller/action/arg
The 'urlPrefix' configuration setting will let you override the default if you would like to use a different variation which your server supports.
You can name the router file something other than 'index.php' if you configure your server to recognize the new file name as the directory index.
URL rewriting is a feature some servers support which enables you to direct requests to a file without naming it in the URL.
http://example.com/controller/action/arg
This enables shorter, neater URLs.
If you use URL rewriting set the 'urlPrefix' configuration to an empty string ''.
If you have an Apache server with mod_rewrite enabled and .htaccess configuration, placing the following in your .htaccess file will direct any requests which are not for files in the 'public' directory to 'index.php'
RewriteEngine on
# Rewrite all URLs not pointing to the public directory
RewriteRule !^public\/ index.php
# Rewrite all remaining URLs which do not have a corresponding file
RewriteCond %{REQUEST_FILENAME} !-s
RewriteCond %{REQUEST_FILENAME} !-l
RewriteRule ^.*$ index.php
This approach is a "hack". URL rewriting is preferrable.
If your server does not support URL rewriting but does support the ability to assign custom 404 (file not found) error documents you can largely mimick URL rewriting with a little bit of hacking.
First you need need to assign the router file as the 404 error document.
If you have an Apache server with .htaccess configuration it will look something like this.
DirectoryIndex index.php ErrorDocument 404 /index.php
The second line which begins with 'ErrorDocument' is the one where index.php gets assigned as the custom 404 document. You will notice that there is a '/' slash preceeding 'index.php'. This represents the relative server directory path to the file. If your site is being served from a relative server directory other than '/' you will need to use that instead.
# If your site is being served from the server directory '/sites/005' DirectoryIndex index.php ErrorDocument 404 /sites/005/index.php
If you are unsure you can place the following code temporarily at the top of 'index.php' and browse to it to view the full file path you will need for the 'ErrorDocument 404' line.
<?php echo $_SERVER['SCRIPT_NAME']; exit;
Next you need to set the 'urlPrefix' configuration to an empty string ''.
While this method works for linking, it does not work for accepting data posted from a form. If you need to accept input from forms there is a work around: use the router file name and query string symbol for form URLs. The following is an example for a view with a form in it.
<-- A standard application link -->
<a href="<?php echo $this->appUrl('test/get-input'); ?>">Refresh</a>
<-- The form action will need to simulate the 'urlPrefix' needed for post data -->
<form action="<?php echo $this->appUrl('index.php?/test/get-input'); ?>" method="post">
<-- etc. -->
</form>
Keep in mind that if you have application URLs which match subdirectories you will get errors. e.g. http://example.com/application/ would try to index the 'application' directory rather than loading ApplicationController. If you had such a controller you could rename the directory, for example by placing an underscore at the beginning of its name, and accounting for it in your configuration.
And just to make things a little more interesting, even if this is working to serve content as expected, the server will send error headers to the browser telling it that it has a 404 not found error. For the sake of correctness you may want to override those headers.
<?php
require('lib/lightrail-1.0.5/lightrail.php');
header('Status: 200 OK');
header('HTTP/1.1 200 OK');
LightRail::run();
When parsing the incoming URL for arguments, instead of relying entirely on query strings LightRail uses URL segments separated by '/' slashes to create more readable URLs.
instead of this:
http://example.com/index.php?controller=catalog&action=view+details&item=sharklaser
this:
http://example.com/index.php?/catalog/view-details/sharklaser
or with URL rewriting, this:
http://example.com/catalog/view-details/sharklaser
The following would also work:
In LightRail application URLs the first segment is used to denote which controller class to load and the second segment is used to denote which action method of the controller to call.
If there are more than two URL segments the third and following segments will be passed as arguments to the selected action method of the selected controller.
Consider the following URL (used in conjunction with URL rewriting):
http://example.com/catalog/view-details/sharklaser
The first URL segment calls for the CatalogController class and the second segment calls for the viewDetailsAction method. The third segment is passed as an argument to the viewDetailsAction method.
<?php
class CatalogController extends LightRail_Controller
{
public function viewDetailsAction($item=null)
{
// $item corresponds to the third URL segment
if ($item == 'sharklaser') {
// This part is up to you
}
}
}
LightRail_Controller also provides a urlSegment() method to access individual URL segments. (an idea from CodeIgniter)
<?php
class CatalogController extends LightRail_Controller
{
public function viewDetailsAction()
{
$seg1 = $this->urlSegment(1); // returns 'catalog'
$seg2 = $this->urlSegment(2); // returns 'view-details'
$seg3 = $this->urlSegment(3); // returns 'sharklaser'
$seg4 = $this->urlSegment(4); // returns null
}
}
If you have query string data you would like to append to a LightRail URL you do not need the '?' symbol to start it.
http://example.com/controller/action/arg1=yes&arg2=no
The same example without URL rewriting:
http://example.com/index.php?/controller/action/arg1=yes&arg2=no
Custom URL 'routes' are not the same thing as the 'router' file.
Normally LightRail takes the first URL segment to select the controller and the second URL segment to select the action method. However, if you wish to use application URL segments which differ from that pattern, custom routes allow you to do so.
Custom routes are set in your configuration to map an incoming application URL to the applied application URL. In other words, you can map a customized application URL to the 'controller/action/arg' pattern LightRail expects.
Direct routes allow custom routing with negligible performance overhead. The routes are supplied to the configuration as a key/value array with the array key representing the incoming application URL and the value representing the applied application URL.
<?php
require('lib/lightrail-1.0.5/lightrail.php');
$conf['routes']['news'] = 'Blog/index/news';
LightRail::run($conf);
In this example the URL
http://example.com/news
gets treated as
http://example.com/blog/index/news
This means it would invoke BlogController::indexAction() with the argument 'news'.
You can also use Perl compatible regular expressions to format custom application URLs. This is much more flexible although it incurs a little more performance overhead, particularly if there is a large number of such routes.
'pregRoutes' are set in your configuration similarly to conventional direct routes except that the array key represents the regular expression to match against and the value represents the replacement string.
These arguments are formatted in accordance with PHP's preg_replace function.
<?php
require('lib/lightrail-1.0.5/lightrail.php');
$conf['pregRoutes']['/^news\/([0-9]{4}\-[0-9]{2}\-[0-9]{2})/'] = 'Blog/index/news/\\1';
LightRail::run($conf);
This example would respond to a URL such as
http://example.com/news/1952-04-01
treating it as
http://example.com/blog/index/news/1952-04-01
As a result LightRail would call BlogController::indexAction() with 'news' supplied as the first argument and '1952-04-01' supplied as the second argument.
LightRail's object relational mapping class is LightRail_Record. It follows a simplified active record pattern and makes it easy to read, write and update database records in an object oriented manner.
Each model corresponds to a database table which is used to contain the records. There are a couple of conventions necessary for model tables:
You can either set the 'id' field to be auto-incrementing or else a sequence table will be used. Auto-incrementing is generally preferrable if your database supports it. Otherwise if PDO has CREATE permission LightRail may be able to create sequence tables automatically.
Each model requires a class name which ends with 'Record'. So if you had a model for working with 'handy items' you might name it 'HandyItemRecord'. Model files are named after the class so the file would be 'application/models/HandyItemRecord.php'.
Example of a model class:
<?php
class HandyItemRecord extends LightRail_Record
{
protected $_table = 'handy_item';
public function validate()
{
$this->validateNotEmpty('name', 'price');
$this->validateNumeric('quantity', 'price');
}
}
And now an example of the model being used in a controller:
$item = new HandyItemRecord();
$item->name = 'My Item';
$item->price = 1.00; // wink
if ($item->save()) {
$_SESSION['notice'] = 'The handy item has been saved';
$this->redirectTo('handy-item/index'); // This is a controller method
}
Some things to note in the preceeding example:
LightRail_Record has the following attributes which may be set explcitly:
You will notice that all of the aformentioned properties begin with '_', the underscore character. For compatibility with LightRail_Record, your database field names should begin with an alphabetical character.
LightRail_Record enables you to define the following methods which will get called automatically:
LightRail_Record has the following predefined methods:
The constructor can also be passed arguments for the 'load()' method to instantiate the model and load a matching record at the same time.
// Return the model object // Also load it with the database record whose 'id' is 12 (if it exists) $item = new HandyItemRecord(12);
LightRail_Record currently includes the following validation methods:
If more than one field is passed to this method the combination of those fields is checked for uniqueness.
LightRail_Record can save a lot of repetitive coding but it also incurs more performance overhead than using PDO directly. You have the option of accessing PDO directly through the static method LightRail::pdo().
// Grab a list of handy items which are in stock $db = LightRail::pdo(); $sql = 'SELECT * FROM handy_item WHERE quantity > 0 ORDER BY name'; $items = $db->query($sql)->findAll();
The controller is the principal logic layer which ties the other layers together.
Each controller requires a class name which ends with 'Controller'. Often a controller is named after a model it works with, so if you had a 'HandyItem' controller for working with the model 'HandyItemRecord' you would name the class 'HandyItemController'. However, the controller would be referred to in configuration, URLs and elsewhere simply as 'HandyItem'.
Controller files are named after the class so the file name for this example would be application/controllers/HandyItemController.php
<?php
class HandyItemController extends LightRail_Controller
{
public function indexAction()
{
// Get an array of all stocked 'handy items' and make it available to the view
$model = new HandyItemRecord();
$this->view->items = $model->find('quantity > 0');
}
}
Some things to note from the preceding example:
As mentioned in the chapter on LightRail URLs, the first two application URL segments select the controller and action.
The following URL invokes HandyItemController::indexAction()
http://example.com/handy-item/index
These would also work:
If you have logic to run before the action method you can define the public method init() which will be called before the action.
class HandyItemController extends LightRail_Controller
{
public function init()
{
// Set the layout template to use with this controller
$this->setLayout('handyItemLayout.phtml');
}
public function indexAction()
{
// Get an array of all stocked 'handy items' and make it available to the view
$model = new HandyItemRecord();
$this->view->items = $model->find('quantity > 0');
}
}
If you have logic or methods to make available to all controllers (or a group of controllers) you can place them in a parent class which extends LightRail_Controller and have the action controller extend your custom class.
<?php
class MyController extends LightRail_Controller
{
// Logic to execute before any action method
public function init()
{
$this->setLayout('site.phtml');
}
// Method available to child classes
protected function _avoidClothingLecture($clothing)
{
if ($clothing->isUgly) {
$clothing->hideFromSpouse();
}
}
}
<?php
class HandyItemController extends MyController
{
public function init()
{
// Remember to call the parent class init method
parent::init();
$model = new HandyItemRecord();
$items = $model->find('quantity > 0 ORDER BY name');
foreach ($items as $item) {
if ($item->type == 'clothing') $this->_avoidClothingLecture($item);
}
}
}
Some things to note from this example:
LightRail_Controller uses the magic method __call() to return a 404 file not found error when a URL requests an action which has no corresponding definition. If you want to override this method you may want to ensure there is still some manner of 404 error handling.
<?php
class HandyItemController extends LightRail_Controller
{
public function __call($method, $args)
{
if ($method == 'deployLaserShark') {
// this part is up to you
} else {
return parent::__call($method, $args);
}
}
}
LightRail_Controller has the following methods:
The view is the presentation/interface layer which is output to the browser. (or other consumer) Normally this is HTML but the view can also be used to output other text or binary content such as images.
Each view is associated with a controller and action. In order for LightRail to automatically find and use a given view it should be named after the action and reside in a directory named after the controller.
View files can end in the extension '.phtml' or '.php'. Some developers prefer to use the '.phtml' extension to differentiate presentation related files from application logic related files.
So the view file for HandyItemController::indexAction() would be:
application/views/HandyItem/index.phtml
LightRail uses PHP for the view as it is compatible with presentational markup and avoids the inherent performance penalty of custom template languages. This does mean it is possible to include logic which is unrelated to presentation but that decision is left up to the developer.
The view contains the HTML content which is output to the browser.
Let us revisit HandyItemController::indexAction()
<?php
class HandyItemController extends LightRail_Controller
{
public function indexAction()
{
// Get a list of handy items which are in stock
$model = new HandyItemRecord();
$this->view->items = $model->find('quantity > 0 ORDER BY name');
}
}
Now the corresponding view:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Handy Items</title>
<link rel="stylesheet" type="text/css" href="<?php echo $this->publicUrl('css/styles.css'); ?>" />
</head>
<body>
<h1>Handy Items</h1>
<ol>
<?php foreach ($this->items as $item): ?>
<li><?php echo $this->escape($item->name); ?></li>
<?php endforeach; ?>
</ol>
</body>
</html>
A few things to note about the preceding example:
A view other than the action default can be selected using the setView() controller method. This can be useful if the same view file is used by more than one action method.
<?php
class HandyItemController extends LightRail_Controller
{
public function indexAction()
{
$model = new HandyItemRecord();
$this->view->items = $model->find();
}
/**
* This action is exactly the same as 'index'
* except that it only indexes large items
*/
public function largeItemIndexAction()
{
// Get a list of large handy items
$model = new HandyItemRecord();
$this->view->items = $model->find('size=?', array('large'));
// The 'index' view will do the job
$this->setView('index');
}
}
View content can be set directly by using the controller method setContentForLayout() instead of a view file. (This method works even if you are not using a layout.)
<?php
class HandyItemController extends LightRail_Controller
{
public function indexAction()
{
// Get a list of handy items which are in stock
$model = new HandyItemRecord();
$items = $model->find('quantity > 0 ORDER BY name');
$html = "<h1>Handy Items</h1>\n"
. "<ol><li>"
. implode("</li><li>", $items)
. "</li></ol>\n";
$this->setContentForLayout($html); // no view file is used
}
}
If a layout is being used LightRail will leave it up to the layout to grab the content using the view method getContentForLayout(). Otherwise LightRail will render the content automatically.
LightRail supports basic layouts. A layout supplies a page template which any number of views can be pulled into. Layouts reside in the 'application/views/layouts/' directory. Example:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title><?php echo $this->title; ?></title>
<link rel="stylesheet" type="text/css" href="<?php echo $this->publicUrl('css/styles.css'); ?>" />
</head>
<body>
<div id="siteNavigation">
<ul>
<li><a href="<?php echo $this->appUrl('handy-item/index'); ?>">Handy Items</a></li>
<li><a href="<?php echo $this->appUrl('handy-item/recommend'); ?>">Recommend an Item</a></li>
</ul>
</div>
<div id="pageContent"><?php echo $this->getContentForLayout(); ?></div>
<div id="copyright">Copyright © 1952 Handy Example Co.</div>
</body>
</html>
Because the layout supplies the larger document structure the view can limit itself to the content requested by the getContentForLayout() method. Example:
<h1>Handy Items</h1>
<ol>
<?php foreach ($this->items as $item): ?>
<li><?php echo $this->escape($item->name); ?></li>
<?php endforeach; ?>
</ol>
A few things to note about the preceding example:
The controller method setLayout() can be used to assign a layout to a controller or individual action as well. setLayout() can also be called with the argument null to instruct LightRail not to use a layout.
<?php
class HandyItemController extends LightRail_Controller
{
public function init()
{
// Use the layout 'handyItem.phtml' for this controller
$this->setLayout('handyItem');
}
// etc.
// The view for this particular action does not use a layout
public function layoutLessAction()
{
$this->setLayout(null);
}
}
The following methods are available in LightRail_View:
View helpers are not currently supported but they may be supported soon in a manner similar to Zend Framework.
In the mean time LightRail_View can be extended with a custom class if desired.
You can always create your own intermediate custom classes to add functionality to the LightRail classes.
If you want your custom class to autoload you can register it with the static method LightRail::autoloadClass($class, $file). This can either be done before calling LightRail::run() or in the controller's init() method.
If you create an intermediate class for LightRail_View you can use it by explicitly setting the 'view' property of the controller in its init() method.
<?php
class HandyItemController extends LightRail_Controller
{
public function init()
{
// Use a customized class for the view
$this->view = new MyView();
}
}
The different classes in LightRail need to share a number of variables and functions between them. To provide global functionality without taking up global namespace the LightRail class contains global static methods both for the framework's own internal use and for users who may want to leverage them directly.
While some of these methods are designed for LightRail's internal use there are a number of methods which might have a use in your router, controllers or models.
Many Zend framework classes are not dependent on Zend framework and may be useful to a LightRail project. (e.g. Zend_Pdf)
LightRail can autoload Zend classes if you add the top level Zend class directory to PHP's include_path setting. One way this can be done conveniently is with the static method LightRail::includePath($path), either calling it in the router file before LightRail::run() or in a specific controller's init() method.