Ohm – Least Resistance
A PHP framework so simple, you can use it before your morning coffee.
- Model-View-Controller architecture – take pride in your designs.
- Class auto-loading – finds your classes, dead or alive.
- Request dispatcher – maps HTTP requests to actions, so you won’t have to.
- Pretty database layer – two nice patterns for dealing with that pesky MySQL.
- Form support – to help support that user interaction addiction of yours.
- Page layouts – the best thing since copy-paste.
- Open Source license – it’s in the public domain, baby!
Download the Latest Version : Ohm.zip or Ohm.tar.gz
Current version is : BETA.
Contact me (victor-ohm@nicollet.net) if you have any questions!
There’s already Zend, Symfony and CodeIgniter… Why Ohm ?
A framework has to support a wide range of projects. Existing frameworks achieve this by being infinitely configurable. Ancient legends say that whatever you’re trying to do, there’s a combination of arcane options you can set to have Zend Framework do it for you.
Consequence 1: you have to learn about those arcane options, hence the insane amount of «How do I do X with framework Y?» questions online.
Consequence 2: to support all those options, the code becomes too large and complex to be used as a practical source of information.
Ohm goes the other way : it contains less than 2000 lines of code, documentation included, and it’s the kind of code you can understand before your morning coffee cup. If Ohm does not support a specific feature you absolutely need for your project, move in and change a few lines of code where it actually matters. It will actually be faster than other modes of framework extension. That’s what Least Resistance means (in addition to being a cheap pun).
And then, there’s the philosophy. Every framework out there has a philosophy, a Right Way of Doing Things that you can agree or disagree with. Somewhere out there, there’s a PHP framework that fits your view of the world, follows the structure of your thoughts and generally pleases your Feng Shui. Ohm might be it.
Interested? Before you start, try reading the…
Ohm Manual
Because you can learn it all on the download page.
Installing and Requirements
Ohm itself requires PHP 5. The default HTTP request dispatcher assumes that mod_rewrite is enabled and you can either use .htaccess files or put the rewrite rules directly in your apache configuration files. The database helpers use the mysqli extension.
To install the system, download one of the two files above and unpack it at the root of your web server. You should be looking at:
.htaccess # redirects requests to index.php index.php # responds to requests by using your code public/ # the files that can be downloaded directly app/ # your code lib/ # library code, including Ohm
That’s it. You’re done. You are expected to put the code you write in the app/ folder.
Ohm comes with a bootstrap index.php file that initializes everything you need, so you can usually start writing useful code right away.
The Class Loader
… automatically attempts to load any undefined class for you.It looks for the class Foo_Bar_Qux in a file named Foo/Bar/Qux.php in any location on your include_path.
There’s a special case, though: class Foo is expected to be in Foo/Root.php. This is done to keep all Foo-related code in the Foo/ directory, which looks cleaner. You can easily disable this by editing the loader file.
The loader is available in lib/Ohm/Loader.php. Assuming that lib/ is in your include_path, you initialize it with:
require 'Ohm/Loader.php'; Ohm_Loader::init();
Please note that index.php already does all of this for you. It also sets up the include path to contain the lib/ and app/ folders. You are expected to put the code you write in the app/ folder.
The Dispatcher
Your application receives HTTP requests. Requests for existing files in the /public directory are handled by apache without running any PHP code. Any other requests are redirected to the index.php file, which responds to them by loading an action class. The loaded class depends on the HTTP request address:
http://domain/uses the classIndex_Actionhttp://domain/foo/bar/quxuses the classFoo_Bar_Qux_Action- If an action class does not exist, uses
E404_Action
If your application has to be placed in a subdirectory, such as http://domain/myApp/, you can edit index.php and replace this line:
$dispatcher = new Ohm_Dispatcher('E404_Action');
With this line:
$dispatcher = new Ohm_Dispatcher('E404_Action',null,'/myApp/');
Actions
… are the core of the Controller layer. They are allowed to explore the HTTP request parameters, the session, the cookies and anything involved with HTTP. They must use the Model layer to access the database, and they must use the View layer to render HTML.
The typical action class looks like this:
class User_Action extends Ohm_Action { protected function _run(array $get, array $post=null) { $user = User::get( (int)$get['id'] ); if (!isset($user)) return self::redirect(Index_Action::url()); $view = new User_View($view); $view -> user = $user; $layout = new Layout($view); return self::html($layout); } static function url($id) { /*[*/ assert (is_int($id)); /*]*/ return Ohm_Dispatcher::url(__CLASS__, compact('id')); } }
It must provide a _run() method that is called when the request is received. The arguments it receives are the GET values and either null (in a GET request) or the POST values (in a POST request). These values are protected against magic quotes (if you don’t know what this means, you don’t need to know anyway).
That function must return a result (constructed by helper functions of Ohm_Action) which will then be executed by the framework. Popular results are Ohm_Action::redirect($url) which performs a 303 See Other to the provided address, Ohm_Action::html($layout) which renders a layout and returns it as UTF-8 HTML, and Ohm_Action::json($data) which serializes the data and returns it as UTF-8 JSON. Never forget to return a result, or bad things will happen.
Action classes may provide url() static functions. These functions usually rely on Ohm_Dispatcher::url($classname,$get) to construct the appropriate address string. This saves you the pain of:
- Retrieving the domain name and port yourself.
- Adding a
/myApp/prefix to all URLs when necessary. - Hunting down all occurrences of an URL while refactoring.
- Having to map a raw address to its action.
- Escaping and formatting the GET parameters for use in an address.
Controller Helpers
If you need to share some code across several actions, you may do so by inheriting those actions from a common base class (not recommended) or placing the shared behavior in a controller helper class.
Either way, any controller code that is not an action is, by convention, suffixed with _Ctrl.
For instance, if you have a page layout that includes the user name in the top left corner, every action would have to load the current user identifier from the session, query the database for the user name, and give that name to the layout. All this code can be placed inside a single class and reused everywhere:
class Layout_Ctrl { static function create(Ohm_View $view) { $id = $_SESSION['current_user']; $user = User::get( $id ); $name = $user->name; $layout = new Layout($view); $layout->welcome = $name; return $layout; } }
If you wish to keep your sanity, it is advised that you do not access the database or generate HTML directly from a controller helper. Use models and views, respectively.
Connecting to a Database
… is as simple as creating a new database resource:
class My_Db { const HOST = '...'; const USER = '...'; const PASS = '...'; const PORT = 3306; const DATABASE = '...'; /** * @return Ohm_Db */ static function open() { return Ohm_Db::open(__CLASS__); } static function close() { Ohm_Db::close(__CLASS__); } /** * @return Ohm_Db_Stmt */ static function query($q,$a=array()) { return self::open()->prepare($q)->bind($a); } }
The database system will automatically look up the class constants and connect to the database the first time you use it. The connection will be cached (so that opening it several times returns the same connection) and kept around until you close it, or until the script ends.
Constructing Queries
Ohm uses prepared statements. Requests are prepared with placeholders for values, bound to arrays that represent the actual values, and executed. This can be done with the open()-prepare()-bind() sequence of functions, or with the query() shorthand illustrated above.
Placeholders are alphanumeric names prefixed with a colon (:) and the bound arrays are mapped to those placeholders based on the keys. This is a typical example:
function isValid($login, $pass) { $q = <<<EOQ SELECT COUNT(*) FROM users WHERE login = :login AND passhash = MD5(CONCAT(:pass),salt) EOQ; $a = compact('login','pass'); return My_Db::query($q,$a)->toInt() != 0; }
The replacement system is smart: you can use a single placeholder several times, values will be escaped against SQL injection and will retain their original type (integer, string, null…).
Important : prepare(), bind() and query() do not execute the statement. Only the subsequent toXXX() function call executes the statement. If you don’t care about what a statement returns, use toVoid().
Extracting Query Results
Data retrieval is done with the toXXX() functions. Each of these executes the query and retrieves the result in a certain format. The function name provides a hint as to what that format is.
->toVoid()does not return any data. This is usually the case with update or delete statements.->toInsertId()returns the value of the last generated value for an auto-increment column. This is used for insert statements that insert a single line.->toInt()returns the first field of the first row as an integer. It returns zero if that field is not an integer, and null if there are no rows or fields. This is usually applied when counting things.->toInts()returns the first field of every row as an array of integers. This is generally useful when fetching a list of integer identifiers from the database.->toRow()returns an associative arrays that map field aliases to field values for the first row, in a manner similar to the standard MySQLfetch_assoc. It returns null if there are no rows, and any rows except the first are discarded.->toRows()returns an array of associative arrays, one for every row in the result set.
You can find advanced and expert techniques for extracting query results in the appendix.
The View Layer
According to MVC design, all HTML generation happens in the View layer, and the view layer may not access anything but the data that was passed to it by the controller. In particular, accessing the session variables, HTTP request data, or the database, are all verboten from views, even if done through a model of controller. The view layer receives data and returns HTML, and that’s it.
Exceptionally, the view layer is allowed to use the ::url() functions of actions to create links to other pages, and is allowed to access form objects to render the fields. But that’s it—don’t get any silly ideas.
The view layer contains Layout classes and View classes. The former handles the outer elements of an HTML page (the doctype, the html, head and body tags, as well as the contents of the head tag), while the latter outputs a bit of HTML that can be placed anywhere in the body.
To refresh your memory, here’s the action example I previously showed you. Note the view and layout parts.
protected function _run(array $get, array $post=null) { $user = User::get( (int)$get['id'] ); if (!isset($user)) return self::redirect(Index_Action::url()); $view = new User_View($view); $view -> user = $user; $layout = new Layout($view); return self::html($layout); }
The View Classes
A view class extends the Ohm_View class, and usually contains a list of variables (what is rendered) and a rendering function (how it is rendered) that directly outputs HTML. For instance:
class User_View extends Ohm_View { /** * @var User */ var $user; protected function _render() { ?> <div class="user"> <span class="user-name"> <?php self::esc($this->user->name) ?> </span> <a href="<?php self::esc(User_List_Action::url()) ?>"> Back to list </a> </div> <?php } }
It’s mostly just printing out HTML using the input variables. Nothing clever. As a shortcut, the class provides an esc() function that is equivalent to calling htmlspecialchars followed by echo.
Note : it’s possible, and encouraged, to call a view’s render() function directly, but doing so requires a layout (a view cannot be rendered outside of a layout). In the case of a view calling another view, this is usually done as $view->render($this->layout()) (using the layout() member function to access the layout of the currently executing view).
The Layout Classes
Unless you have a good reason not to, you should use Ohm_Layout_Page as a layout class, either directly or by extending it with inheritance. A typical extension is:
class Layout extends Ohm_Layout_Page { /** * @var User */ var $user; function __construct(Ohm_View $view) { parent::__construct($view); $this->addCssFile('/public/css/custom.css'); $this->addJsFile('/public/js/jquery.js'); } protected function _renderInner() { ?> <div id="header"> Welcome, <?php Ohm_View::esc($this->user->name) ?> </div> <div id="content"> <?php parent::_renderInner() ?> </div> <?php } }
The two things that can be extended:
- The constructor can be extended to automatically include some CSS and JavaScript files, as well as code.
- The _renderInner function can be extended to surround the HTML inside the body with additional HTML elements, calling the parent version of itself to render the HTML provided by the view.
The layout class provides functions to manipulate the header, JavaScript and CSS. These can be called from the layout constructor (but not other layout functions) as $this->addCssFile(...) and from the view as $this->layout()->addCssFile(...).
These functions are:
->addCssFile($url): adds a stylesheet link to the provided file to the header for all media.->addJsFile($url): adds a script tag right before the end of the body tag including the provided file.->addCssCode($code): writes some CSS to a style tag in the header.->addJsCode($code): writes some JS code in a script tag at the end of the body tag, inside a CDATA block.->addHeadHtml($html): writes some HTML code as-is right before the end of the head tag.
Appendix
(You may skip this part if you wish to)
Extracting Query Results – Advanced Techniques
For type-safety and self-documenting code purposes, Ohm allows retrieving rows as objects of a certain class instead of plain associative arrays. This is done by passing the class name to the row extraction functions toRow() and toRows().
If a class name is provided, then instead of returning an associative array, the row functions will instantiate the class for every row, passing an Ohm_Db_Row as the only constructor argument. The constructor is then responsible for extracting any relevant data from the row object.
One way of doing so is calling the row’s toAssoc() member function, which returns an associative array from which you may initialize the object members. Note that associative array values are strings, so you might have to do some conversion work to do to obtain integers, real numbers, booleans or timestamps.
Another way is to attach the row to your object by calling the into($obj) function (which tells it that it should write to the member variables of that object) then calling the conversion functions:
$row->string('x')reads the value of field'x'(a string or null value) and writes it to member variable$obj->x.$row->int('x')does the same, but first converts the value to an integer (but null values remain).$row->bool('x')does the same for booleans.$row->time('x')does the same for date and datetime values, converting them to an Unix timestamp integer.
All these functions may take a second argument to indicate where the data should be written. For instance, $row->string('x','y') reads the value of field 'x' and writes it to $obj->y.
The conversion functions support the method chaining pattern by returning the original row.
The example below shows the pattern for mapping a class to a database table:
CREATE TABLE test ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, testname VARCHAR(100) NOT NULL ); class Test { var $id; var $name; function __construct(Ohm_Db_Row $row=null) { if ($row) { $row->into($this) ->int('id') ->string('testname','name'); } } /** * @return Test */ function get($id) { $q = 'SELECT * FROM test WHERE id = :id'; $a = compact('id'); return My_Db::run($q,$a)->toRow(__CLASS__); } /** * @return array Test */ function getAll() { $q = 'SELECT * FROM test'; return My_Db::run($q)->toRows(__CLASS__); } }
Extracting Query Results – Expert Techniques
One indirect benefit of using the conversion methods (instead of toAssoc) is that you can handle situations where several fields, coming from different tables, have the same name: into() lets you provide the name of the source table as a second argument.
This is the idiomatic example:
CREATE TABLE page ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL ); CREATE TABLE link ( page_linker INT NOT NULL, page_linked INT NOT NULL, PRIMARY KEY (page_linker, page_linked) ); class Page { var $id; var $name; function __construct(Ohm_Db_Row $row=null, $tbl='page') { if ($row) { $row->into($this,$tbl)->int('id')->string('name'); } } } class Link { var $linker; var $linked; function __construct(Ohm_Db_Row $row=null) { if ($row) { $this->linker = new Page($row, 'linker'); $this->linked = new Page($row, 'linked'); } } /** * @return array Link */ function getAll() { $q = <<<EOQ SELECT * FROM link INNER JOIN page linker ON linker.id = link.page_linker INNER JOIN page linked ON linked.id = link.page_linked EOQ; return My_Db::query($q)->toRows(__CLASS__); } }
In this example, $link->linked->name and $link->linker->name will be the correct values, even though the two fields had the same name in the request.
Hi. I'm Victor Nicollet,
Recent Comments