This post aims to be a very basic introduction to the world of Lithium, also known as li3. As such, it is mostly based on the ubiquitous blog sample. The idea is to learn, through the source code, the basic notions on Lithium and its integration with the Doctrine ORM mapper to build a very basic blog application. Unlike the blog example at rad-dev.org, this example is based on Doctrine 2.0, which is (at the time of this writing) in Alpha stage, and is built for PHP 5.3. After this tutorial, you should be able to jump start and build your own li3 powered applications.
Before we proceed, a disclaimer. This tutorial shows a rather quick and dirty integration with Doctrine (as you’ll see it’s all mostly done in a base model class), while the most clean approach would be to implement all this as an extension, using Datasources and Query parsing. I’m happy to say this is all being done as we speak (thanks kuja!), so very soon you’ll find an even better approach to Doctrine integration.
Other Lithium Tutorials
Of course I’m not the only one to have written about Lithium! Here are some tutorials you may enjoy:
- Video Tutorial: li3 on Apache2, and li3 on IIS7.
- The original Lithium blog tutorial.
The Basics
For the sakes of me being lazy, I’ll assume you have prior knowledge of:
- PHP 5.3 basics
- Another PHP framework (like CakePHP, the most popular PHP framework).
- Using GIT (you should have it installed, Ubunty folks:
$ sudo apt-get install git-core) - You should have an account setup with your SSH key in rad-dev.org (instructions here)
Adding PHP 5.3 support
There are many ways people have shown how to enable PHP 5.3 support on your servers. In my case, since I run my own compiled version of Apache + PHP, it gets easier. I’ll show you here how to set up dual support (PHP 5.2 and PHP 5.3) on your Apache in case you are as smart as me, and decided to build it yourself as well ;-]
First download and compile PHP 5.3 (this instructions point to the latest release of PHP 5.3 at the time of this writing, you may need to check PHP.net to see if there’s a newer version):
$ cd /usr/local/src $ sudo wget http://ar.php.net/get/php-5.3.1.tar.gz/from/this/mirror $ sudo tar xzvf php-5.3.1.tar.gz $ sudo chown -R $USER:$USER php-5.3.1/ $ cd php-5.3.1/ $ ./configure \ --prefix=/usr/local/php5.3 \ --with-config-file-path=/usr/local/php5.3 \ --with-libxml-dir=/usr/include/libxml2 \ --with-mysql=/usr/local/mysql \ --enable-pdo \ --with-pdo-mysql=/usr/local/mysql \ --with-zlib=/usr/local/zlib \ --with-curl \ --with-gd \ --with-jpeg-dir=/usr/lib \ --with-png-dir=/usr/lib \ --with-freetype-dir=/usr/lib \ --with-gettext \ --enable-mbstring \ --enable-soap \ --enable-ftp \ --with-openssl $ make $ sudo make install
Easy right? Now, whenever you want to add a virtual host that supports PHP 5.3, you’ll have to add these lines inside the <VirtualHost> directive:
ScriptAlias /cgi-bin/ /usr/local/php5.3/bin/ Action php53-cgi /cgi-bin/php-cgi AddHandler php53-cgi .php5 .php <Directory "/usr/local/php5.3/bin"> Options +ExecCGI -Includes Order allow,deny Allow from all </Directory>
Installing Lithium
First thing you’ll have to do is clone from lithium’s git repository. We’ll do it in a generic place, that we’ll be able to share from all our lithium applications, in order to keep up with the amazing ongoing development in Lithium. So let’s clone that stuff:
$ cd /var/www $ git clone code@rad-dev.org:lithium.git lithium
So now in our /var/www/lithium directory we have lithium installed. Every time we’ll want to ugrade to lithium’s latest release, we’ll just do:
$ cd /var/www/lithium $ git pull --rebase
Setup the application structure
We now have a generic place (/var/www/lithium) where we have lithium installed. Each time we start a new application, we’ll just copy over the application directory, and link to lithium’s core directory.
So we want to create an application in the host http://blog.li3 so we’ll store its files in /var/www/blog.li3. Let’s copy the files, and link to the core:
$ mkdir /var/www/blog.li3 $ cd /var/www/blog.li3 $ cp -pR /var/www/lithium/app . $ ln -s /var/www/lithium/libraries libraries
Add the virtual host
Ok we now have the application installed in /var/www/blog.li3. Let’s create the virtual host for it. First edit your /etc/hosts file and add this line:
127.0.1.1 blog.li3
Now let’s add the virtual host to apache. Edit the file /usr/local/apache2/conf/extra/httpd-vhosts.conf (obviously this may be different in your setup) and add the following:
<VirtualHost *:80> ServerName blog.li3 DocumentRoot "/var/www/blog.li3/app/webroot" ErrorLog "logs/blog.li3.error.log" CustomLog "logs/blog.li3.access.log" combined ScriptAlias /cgi-bin/ /usr/local/php5.3/bin/ Action php53-cgi /cgi-bin/php-cgi AddHandler php53-cgi .php5 .php <Directory "/usr/local/php5.3/bin"> Options +ExecCGI -Includes Order allow,deny Allow from all </Directory> </VirtualHost>
You should now restart apache:
$ sudo /etc/init.d/apachectl restart
And you should be able to browse to http://blog.li3 and see a screen just like this:
Install Doctrine 2.0
We’ll install doctrine as a system-wide library. Therefore we’ll move to our generic lithium install’s library path (/var/www/lithium/libraries) and download doctrine inside. Now we need the Doctrine ORM release (you may need to check the doctrine download page for a more up-to-date version of the 2.0 branch:
$ cd /var/www/lithium/libraries $ mkdir doctrine $ cd doctrine $ wget http://www.doctrine-project.org/download/2_0_0_ALPHA3/format/tgz/package/ORM $ tar -xzvf DoctrineORM-2.0.0-ALPHA3.tgz
Loading the Database
Now that we have the basics in place (PHP 5.3 enabled, Lithium installed, Doctrine installed), we’ll start building our application.
Creating the Database and loading some data
Let’s start by creating a database named blog_li3:
$ mysqladmin -uroot -p create blog_li3
Now load this SQL statements to the database we’ve just created:
CREATE TABLE `posts`(
`id` INT NOT NULL AUTO_INCREMENT,
`title` VARCHAR(255) NOT NULL,
`body` TEXT NOT NULL,
`published` BOOLEAN NOT NULL default FALSE,
`created` DATETIME NOT NULL,
`modified` DATETIME NOT NULL,
PRIMARY KEY(`id`)
);
CREATE TABLE `comments`(
`id` INT NOT NULL AUTO_INCREMENT,
`post_id` INT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`body` TEXT NOT NULL,
`created` DATETIME NOT NULL,
`modified` DATETIME NOT NULL,
PRIMARY KEY(`id`)
);
ALTER TABLE `comments`
ADD KEY `post_id`(`post_id`),
ADD CONSTRAINT `comments__posts` FOREIGN KEY (`post_id`) REFERENCES `posts`(`id`);
INSERT INTO `posts` VALUES
(1, 'First post', 'This is the body for the first post', TRUE, NOW(), NOW());
INSERT INTO `comments` VALUES
(1, 1, 'Mariano Iglesias', 'mariano@email.com', 'This is a nice post!', NOW(), NOW());
Setting up the DB connection
Let’s add the basics Doctrine needs to run into Lithium’s bootstrap. Edit the file app/config/bootstrap.php in your application directory (/var/www/blog.li3) and add the following at the end:
/**
* Load Doctrine
*/
Libraries::add('Doctrine', array(
'path' => LITHIUM_LIBRARY_PATH . '/doctrine/DoctrineORM-2.0.0/Doctrine'
));
Now how easy was that? Since Doctrine shares the same name structure Lithium does, the inclusion of Doctrine classes in Lithium is that easy. Next step: let’s set up the connection. Open app/config/connections.php and replace the whole Connections::add expression with the following (notice how we are adding a connection of type ‘doctrine’ rather than the default type ‘database’ lithium provides):
Connections::add('default', 'doctrine', array(
'driver' => 'pdo_mysql',
'host' => 'localhost',
'user' => 'root',
'password' => 'password',
'dbname' => 'blog_li3'
));
Creating a wrapper for some Doctrine functionality
If you go through doctrine documentation, you’ll see it’s quite powerful and flexible. However, we’ll want to have a basic wrapper around the most common functionality (establishing the connection, querying a model, creating / editing / deleting records) so our work from our controllers is more transparent, and Doctrine agnostic. So let’s create a base class for all models (CakePHP folks, this will sound familiar) and name it AppModel
First, the code. Create a file named AppModel.php in your app/ directory, with the following contents.
<?php
namespace app;
use \lithium\data\Connections;
abstract class AppModel extends \lithium\data\model\Record {
protected static $_alias;
protected static $_entityManagers = array();
public static function create($data = array()) {
$class = get_called_class();
$record = new $class();
return $record->set($data);
}
public function set($data = array()){
if (isset($data) && count($data) > 1) {
if (!isset($data['created']) && !isset($data['id']) && !isset($this->id)) {
$data['created'] = new \DateTime(date('Y-m-d H:i:s'));
}
if (!isset($data['modified'])) {
$data['modified'] = new \DateTime(date('Y-m-d H:i:s'));
}
foreach($data as $key => $value) {
$this->$key = $value;
}
}
return $this;
}
public function save() {
$entityManager = $this->_entityManager();
$entityManager->persist($this);
$entityManager->flush();
return $this;
}
public function delete($id = null) {
if (empty($id)) {
$id = $this->id;
}
$entityManager = $this->_entityManager();
$entityManager->remove($this);
$entityManager->flush();
return $this;
}
public static function find($type = 'first', $query = array()) {
$class = get_called_class();
$alias = self::$_alias ?: substr($class, strrpos($class, '\\') + 1);
$methods = array('first', 'all');
if (!in_array($type, $methods)) {
throw new \Exception("Find method \"{$type}\" is not implemented");
}
$query = array_merge(array(
'fields' => null,
'conditions' => null,
'order' => null
), $query);
$fields = array();
if (isset($query['fields'])) {
foreach($query['fields'] as $field) {
$model = $alias;
if (strpos($field, '.') === false) {
list($model, $field) = explode('.', $field);
}
}
} else {
$fields[] = $alias;
}
$queryBuilder = self::_entityManager()->createQueryBuilder();
$queryBuilder->add('select', $alias)->add('from', $queryBuilder->expr()->from($class, $alias));
$expression = self::_conditionsToDqlExpr($alias, $queryBuilder, $query['conditions']);
if (isset($expression)) {
$queryBuilder->add('where', $expression);
}
if (!empty($query['order'])) {
$order = $query['order'];
if (is_array($query['order'])) {
$order = array();
foreach($query['order'] as $field => $direction) {
if (!is_string($field)) {
$field = $direction;
$direction = 'asc';
}
$order[] = (strpos($field, '.') === false ? $alias . '.' : '') . $field . ' ' . $direction;
}
$order = implode(',', $order);
}
$queryBuilder->add('orderBy', $order);
}
$dql = $queryBuilder->getDql();
$result = self::_entityManager()->createQuery($dql)->getResult();
if (isset($result) && $type == 'first') {
$result = current($result);
}
return $result;
}
protected static function _conditionsToDqlExpr($alias, $queryBuilder, $conditions) {
$expr = $queryBuilder->expr();
if (empty($conditions)) {
return null;
} else if (is_string($conditions)) {
$expr = $expr->andx($conditions);
} else {
foreach($conditions as $key => $value) {
if (is_string($key) && in_array(strtolower($key), array('and', 'or'))) {
$clause = strtolower($key);
$pieces = array();
foreach((array) $value as $innerKey => $piece) {
if (is_string($innerKey)) {
$piece = array($innerKey => $piece);
}
$pieces[] = self::_conditionsToDqlExpr($alias, $queryBuilder, $piece);
}
$expr = call_user_func_array(array($expr, $clause . 'x'), $pieces);
} else if (is_string($key)) {
if (strpos($key, '.') === false) {
$key = $alias . '.' . $key;
}
if (is_array($value)) {
$values = $value;
foreach($values as $iv => $value) {
$values[$iv] = $expr->literal($value);
}
$expr = $expr->in($key, $values);
} else {
$expr = $expr->eq($key, $expr->literal($value));
}
} else {
$expr = $expr->andx(self::_conditionsToDqlExpr($alias, $queryBuilder, $value));
}
}
}
return $expr;
}
protected static function _entityManager($connectionName = 'default') {
if (!isset(self::$_entityManagers[$connectionName])) {
$connection = Connections::get($connectionName, array('config'=>true));
if (!isset($connection)) {
throw new \Exception("Configuration {$connectionName} not found");
} else if ($connection['type'] != 'doctrine') {
throw new \Exception("Configuration {$connectionName} is not a doctrine configuration");
}
$config = new \Doctrine\ORM\Configuration();
$config->setProxyDir(LITHIUM_APP_PATH . '/models');
$config->setProxyNamespace('app\models');
self::$_entityManagers[$connectionName] = \Doctrine\ORM\EntityManager::create($connection, $config);
}
return self::$_entityManagers[$connectionName];
}
/**
* Overrides for lithium/data/model/Record
*/
public function exists() {
return !empty($this->id);
}
public function __get($name) {
return isset($this->$name) ? $this->$name : null;
}
}
?>
That’s a lot, right? First of all, this is generic enough so we won’t go through the detailed explanation line by line, but we’ll go through what each method does, and why is there. So let’s see:
create(): creates a new instance of a record class, loading it with the specified data.set(): sets the specified data on the current record instancesave(): saves (persists) the current recorddelete(): deletes the current record, or the record whose ID is specifiedfind(): it finds :-] It tries to mimic the same parameters CakePHP has on its find method. Takes two parameters: first, the find type (only ‘first’ and ‘all’ supported here), and second an array of options (supported options: ‘fields’, ‘conditions’, ‘order’)_conditionsToDqlExpr(): helper method used by find() to convert the array based conditions into a Doctrine DQL Query (using Doctrine’s QueryBuilder)_entityManager(): gives back Doctrine’s EntityManager used to perform actual operationsexists(): Tells if current record exists. Overriden from Lithium’s record to add support for the record in the FormHelper__get(): Gives back the given field’s value for current record. Overriden from Lithium’s record to add support for the record in the FormHelper
The models
Ok now to the fun part, creating the models our application will need. If you remember, the SQL script created two tables: posts and comments. Therefore we’ll have two models, Post and Comment. In order to link each comment’s property to a real table field, we’ll use Doctrine’s annotations. They define the characteristics of each record field through source code comments.
Let’s first create the Post model. Create a file named Post.php in your app/models folder with the following contents:
<?php
namespace app\models;
/**
* @Entity
* @Table(name="posts")
*/
class Post extends \app\AppModel {
/**
* @Id @Column(type="integer")
* @GeneratedValue(strategy="AUTO")
*/
public $id;
/** @Column(type="string") */
public $title;
/** @Column(type="text") */
public $body;
/** @Column(type="datetime") */
public $created;
/** @Column(type="datetime") */
public $modified;
/** @OneToMany(targetEntity="Comment", mappedBy="post", cascade={"remove"}) */
public $comments;
}
?>
To understand the actual annotations, go through Doctrine’s introduction to docblock annotations. What it’s important for us is seeing that we have a property for each field in our posts table (what we want mapped to our record class), plus a property to store all related comments (the $comments property), each of which will be an instance of the Comment class.
So what about the Comment class? Create a file named Comment.php in your app/models folder with the following contents:
<?php
namespace app\models;
/**
* @Entity
* @Table(name="comments")
*/
class Comment extends \app\AppModel {
/**
* @Id @Column(type="integer")
* @GeneratedValue(strategy="AUTO")
*/
public $id;
/** @Column(type="string") */
public $name;
/** @Column(type="string") */
public $email;
/** @Column(type="text") */
public $body;
/** @Column(type="datetime") */
public $created;
/** @Column(type="datetime") */
public $modified;
/**
* @ManyToOne(targetEntity="Post")
* @JoinColumn(name="post_id", referencedColumnName="id")
*/
public $post;
}
?>
Just as before, we have a property per field, plus a property named $post to hold the instance of the Post a Comment belongs to.
The Controllers
Let’s first create the basic controller for Post records, that will simply show the list of post titles, with links for operations that we don’t yet have (editing, adding, removing, etc.) Create a file named PostsController.php in your app/controllers folder with the following contents.
<?php
namespace app\controllers;
use app\models\Post;
class PostsController extends \lithium\action\Controller {
public function index() {
$posts = Post::find('all');
$this->set(compact('posts'));
}
}
?>
That’s a very simple controller, isn’t it? We just get the posts, and pass it to the view as a $posts variable. So let’s create the view then. Create a folder named posts in your app/views/ folder, and then create a file named index.html.php in the newly created app/views/posts folder with the following contents:
<h1>Posts</h1>
<p><?php echo $this->html->link('Add new Post', '/posts/edit'); ?></p>
<ul>
<?php foreach($posts as $post) { ?>
<li>
<?php echo $this->html->link($post->title, '/posts/view/' . $post->id); ?>
[<?php echo $this->html->link('Edit', '/posts/edit/' . $post->id); ?>]
[<?php echo $this->html->link('Delete', '/posts/delete/' . $post->id, array('onClick'=>'return confirm("Are you sure you want to delete this record");')); ?>]
</li>
<?php } ?>
</ul>
This code also speaks for itself. So let’s now create an action to view an actual post. Add a method named view() in your PostsController with the following code:
public function view($id = null) {
$id = $id ?: $this->request->id;
$post = Post::find('first', array('conditions' => compact('id')));
$this->set(compact('post'));
}
We are basically getting the ID first (since it’s a numeric ID, a convenient Lithium route check will set it as part of the request information), and then looking for a Post with the given ID. That post will also contain the comments attached to it, so knowing that we can create the view for this action. Create a file named view.html.php in your app/views/posts folder with the following contents:
<h1><?php echo $post->title; ?></h1>
<p><small><?php echo $post->created->format('F d, Y H:i'); ?></small></p>
<?php echo $post->body; ?>
<?php if (!$post->comments->isEmpty()) { ?>
<h3>Comments</h3>
<?php foreach($post->comments as $comment) { ?>
<p><small>
by <a href="mailto:<?php echo $comment->email; ?>"><?php echo $comment->name; ?></a>
on <?php echo $comment->created->format('F d, Y H:i'); ?>
</small></p>
<?php echo $comment->body; ?>
<?php } ?>
<?php } ?>
<h3>Add your Comment</h3>
<?php echo $this->form->create(null, array('url'=>array('controller'=>'comments', 'action'=>'add'))); ?>
<?php echo $this->form->hidden('post_id', array('value' => $post->id)); ?>
<label>Your email:</label> <?php echo $this->form->text('email', array('size'=>60)); ?>
<label>Your name:</label> <?php echo $this->form->text('name', array('size'=>60)); ?>
<label>Comment:</label> <?php echo $this->form->textarea('body', array('rows'=>5, 'cols'=>60)); ?>
<?php echo $this->form->submit('Add Comment'); ?>
<?php echo $this->form->end(); ?>
This code should also speak for itself, except probably the isEmpty() method, which is part of the collection’s methods Doctrine provides.
Another thing to note in the above view is the form at the bottom, used to add comments to a post. First thing we notice there is that we are setting as action for the post an action called add in a controller named Comments. We don’t yet have this controller, do we? So let’s add it.
Create a file named CommentsController.php in your app/controllers folder with the following contents:
<?php
namespace app\controllers;
use app\models\Post;
use app\models\Comment;
class CommentsController extends \lithium\action\Controller {
public function add() {
if (empty($this->request->data)) {
$this->redirect('/');
}
$post = Post::find('first', array(
'conditions'=>array('Post.id' => $this->request->data['post_id'])
));
if (!$post) {
throw new \Exception("No post found for ID {$this->request->data['post_id']}");
}
$comment = Comment::create($this->request->data);
$comment->post = $post;
$comment->save();
$this->redirect('/posts/view/' . $post->id);
}
}
?>
This action looks more involved. First thing we do, is make sure someone is posting data to this action (that’s the check on $this->request->data, where the posted data comes in Lithium). If they don’t, we kick their butts. Next, we make sure there’s an actual post for the given ID. If there’s not, we throw an exception. The last part is the actual saving of the comment, so let’s go through that:
- With
$comment = Comment::create($this->request->data);we create an instance of theCommentclass, initially populating with the posted data (so the properties$email,$name, etc. will get populated with the posted data) - Next with
$comment->post = $post;we specify to whichPosttheCommentbelongs to. - Finally we persist (save) the
Commentwith$comment->save();
Since we redirect them no matter what, we need no view for this action. Also, you may have noticed that there’s no validation. That will be left as homework for you. After the comment is saved, we redirect the user back to the post page.
So we now know how to show the list of posts, how to view a post, and add a comment. All we have to do next is add the ability to add/edit posts, and remove them. Let’s start with the easy part, deleting a post. Add the following to your PostsController:
public function delete($id = null) {
$id = $id ?: $this->request->id;
if (!isset($id)) {
$this->redirect('/posts');
}
$post = Post::find('first', array('conditions' => compact('id')));
if (!$post) {
throw new \Exception("No post found for ID {$id}");
}
$post->delete();
$this->redirect('/posts');
}
This code almost needs no explaining after what we’ve seen so far. The only important part there is the actual deletion of the Post by calling its delete() method. Also, this is another action that needs no view, since we are redirecting them after the operation.
Finally, and since we have seen how to add a comment, adding a post won’t be that different. In fact, we’ll deal with adding and editing posts in the same action.
Add the following action to your PostsController:
public function edit($id = null) {
$id = $id ?: $this->request->id;
if (isset($id)) {
$post = Post::find('first', array('conditions' => compact('id')));
if (!$post) {
throw new \Exception("No post found for ID {$id}");
}
} else {
$post = Post::create();
}
if (!empty($this->request->data)) {
$post->set($this->request->data)->save();
if (!isset($id)) {
$this->redirect('/posts');
}
$this->redirect('/posts/view/' . $post->id);
}
$this->set(compact('post'));
}
The important thing to notice here is that if there’s an ID provided, we try to get the Post. If there’s none, we create a new Post record instance by calling Post::create();. Then, once we got submitted data, we simply populate the properties with posted data and save the record, all in a single line: $post->set($this->request->data)->save();. Yeah, there’s no validation here either. More homework!
So what about the view for this action? Create a file named edit.html.php in your app/views/posts folder with the following contents:
<h3>Edit Post</h3>
<?php echo $this->form->create($post, array('url'=>'/posts/edit/' . $post->id, 'method'=>'post')); ?>
<label>Title:</label> <?php echo $this->form->text('title', array('size'=>60)); ?>
<label>Body:</label> <?php echo $this->form->textarea('body', array('rows'=>5, 'cols'=>60)); ?>
<?php echo $this->form->submit('Add Comment'); ?>
<?php echo $this->form->end(); ?>
This is yet another code that speaks for itself. How lazy am I?
Conclusion
We've had fun, didn't we? We now have a very basic blog system working, using PHP 5.3 only tools (Lithium, the most rad framework, and Doctrine 2, a very interesting ORM). We could easily extend this further, but we'll wait on the work the Lithium team is doing to offer a much better integration with Doctrine. In the meantime, use this knowledge to have some fun, and build some cool projects!
Related posts:







新年早々、Lithium0.4がリリースされました! : candycane development blog wrote:
[...] IglesiasがDoctrineORマッパをLithiumと共に使うチュートリアルを執筆しました。 [...]
Link
Johannes wrote:
Hi Mariano,
thnx for this article which I just used in order to install PHP 5.3 in parallel mode yet. While setting up a virtual host I always ran into the following Apache error: “Invalid command ‘Action’, perhaps misspelled or defined by a module not included in the server configuration”. Finally, I had to enable the action module: $ sudo a2enmod actions
Maybe this is useful information for other readers.
Cheers
Link
Chris wrote:
Awesome cannot wait for better integration with doctrine ^^
Does li3 also support nested modules kinda like Yii ?
Link
Julian Davchev wrote:
Hi,
I had debian with php package and apache 2.2+ and just wanted to add custom complied php5.3 support.
In your vhost there should be one more directive for this to work
SetHandler none
For more info http://www.php.net/manual/en/install.unix.apache2.php
[quote]…Another interesting point with Apache 2.2 is following.
Let suppose we installed PHP as module. But for some directory, we need to use PHP as CGI…[quote]
Link
Julian Davchev wrote:
Guess have to fiix this blog not to use strip tags but rather properly escape things on output (comments I mean). Most important part of my previous post is missing
Link
Fotoğrafçı wrote:
thanks. ı tried it. it helped for my project.
Link