The longer Lithium Blog tutorial using MySQL – Part 1

Lithium is a lightweight, fast, flexible framework for PHP 5.3+. It is still in dev release state but is under active development. This post, as the title suggests, is to introduce you to this new framework using the typical blog tutorial.

Getting started
The first thing is to get the Lithium source code. You can either download the latest release from their download page or clone the git repository using -

CODE:
  1. git clone code@rad-dev.org:lithium.git lithium

In both the cases, place the framework your webroot like /var/www/html or any equivalent location on your setup. Access the URL in browser and follow the instructions to setup the framework before we can actually begin with blog tutorial. When everything is setup correctly you should see a page like

Create database
Let's first create a database. I have named my database as li_blog. Feel free to use the name of your choice. Now create the table required for our tutorial and populate it with some default data using the following SQL

SQL:
  1. CREATE TABLE IF NOT EXISTS `posts` (
  2. `id` int(11) NOT NULL AUTO_INCREMENT,
  3. `title` varchar(255) NOT NULL,
  4. `body` text NOT NULL,
  5. `created` datetime DEFAULT NULL,
  6. `modified` datetime DEFAULT NULL,
  7. PRIMARY KEY  (`id`)
  8. );
  9.  
  10. INSERT INTO `posts` (`id`, `title`, `body`, `created`, `modified`) VALUES
  11. (1, 'First Post', 'This is post number 1', NOW(), NULL),
  12. (2, 'Second Post', 'This is post number two.\r\n\r\nSome content for this post', NOW(), NULL),
  13. (3, 'Third post', 'This is my third post.', NOW(), NULL),
  14. (4, 'Forth Post', 'This is the forth post.', NOW(), NULL),
  15. (5, 'Fifth Post', 'This is post number 5.', NOW(), NULL),
  16. (6, 'Sixth Post', 'My post number 6.', NOW(), NULL);

Routing

Open your app/config/route.php and replace the last section with the following. This is required in order to allow the pagination for index page to work

PHP:
  1. /**
  2. * Finally, connect the default routes.
  3. */
  4. Router::connect('/{:controller}/{:action}/{:id:[0-9]+}.{:type}', array('id' => null));
  5. //Router::connect('/{:controller}/{:action}/{:id:[0-9]+}');
  6.  
  7. Router::connect('/{:controller}/{:action}/page:{:page:[0-9]+}');
  8. Router::connect('/{:controller}/{:action}/page:{:page}/limit:{:limit}');
  9.  
  10. Router::connect('/{:controller}/{:action}/{:args}');

Displaying list of posts

Now that we already have some default posts in our database, let's begin with displaying the list of posts in index page. We will be showing 5 posts per page in descending order of creation date. For that lets begin with creating a model. Create a new file in your app/models and name it Post.php and add the following code to it

PHP:
  1. <?php
  2.  
  3. namespace app\models;
  4.  
  5. use \lithium\util\Validator;
  6. use \lithium\data\Connections;
  7.  
  8. class Post extends \lithium\data\Model {
  9.     public static function __init(array $options = array()) {
  10.         parent::__init($options);
  11.         $self = static::_instance();
  12.  
  13.         $self->_finders['count'] = function($self, $params, $chain) use (&$query, &$classes) {
  14.             $db = Connections::get($self::meta('connection'));
  15.             $records = $db->read('SELECT count(*) as count FROM posts', array('return' => 'array'));
  16.  
  17.             return $records[0]['count'];
  18.         };
  19.     }
  20. }
  21. ?>

Notice that we have written a custom finder to count the number of posts. This function will get executed whenever we call find('count') on Post.

Now lets create a new file in app/controllers and name it PostsController.php Add the following code to the file

PHP:
  1. <?php
  2. namespace app\controllers;
  3. use app\models\Post;
  4.  
  5. class PostsController extends \lithium\action\Controller {
  6.     public function index() {
  7.         $page = 1;
  8.         $limit = 5;
  9.         $order = 'created desc';
  10.  
  11.         if (isset($this->request->params['page'])) {
  12.             $page = $this->request->params['page'];
  13.  
  14.             if (!empty($this->request->params['limit'])) {
  15.                 $limit = $this->request->params['limit'];
  16.             }
  17.         }
  18.  
  19.         $offset = ($page - 1) * $limit;
  20.         $total = Post::find('count');
  21.  
  22.         $posts = Post::find('all', compact('conditions', 'limit', 'offset', 'order'))->to('array');
  23.  
  24.         $title = 'Home';
  25.  
  26.         return compact('posts', 'limit', 'page', 'total', 'title');
  27.     }
  28. }
  29. ?>

Almost all code here is self explanatory. Note that every action must return the variables to make them available in views. Also, by default, find() returns the object of model. We are calling a method to('array') for convenience.

Now that we have all the data required to display the list of posts, lets create a view for displaying it. Create a new fie in app/views/posts (create the directory posts first) and name it index.html.php. Note that Lithium follows some naming conventions and all the filenames and class names must be according to those conventions only. Add the following code to the newly created view file

PHP:
  1. <?php foreach($posts as $post): ?>
  2.  
  3. <article>
  4.     <h1><?= $this->html->link($post['title'], 'posts/view/'.$post['id']); ?></h1>
  5.     <p><?=$this->html->link('Edit', array('controller' => 'posts', 'action' => 'edit', 'args' => array($post['id']))); ?></p>
  6.     <p><?=$post['body'] ?></p>
  7. </article>
  8. <?php endforeach; ?>
  9. <!-- Pagination section -->
  10. <div id="pagination">
  11.     <p class="next floated"><?php
  12.         if ($total <= $limit || $page == 1) {
  13.             //echo '<li>Next Entries &rarr;';
  14.         } else {
  15.             echo $this->html->link('Next Entries &rarr;', array(
  16.                 'controller' => 'posts', 'action' => 'index',
  17.                  'page' => $page - 1, 'limit' => $limit
  18.             ), array('escape' => false));
  19.  
  20.         } ?>
  21.     </p>
  22.     <p class="prev"><?php
  23.         if ($total <= $limit || $page == ceil($total/$limit)) {
  24.             //echo '&larr; Previous Entries</li>';
  25.         } else {
  26.             echo $this->html->link('&larr; Previous Entries', array(
  27.                 'controller' => 'posts', 'action' => 'index',
  28.                 'page' => $page + 1, 'limit' => $limit
  29.             ), array('escape' => false));
  30.         }?>
  31.     </p>
  32. </div>

Lithium comes with default helpers for html and form rendering and are auto-loaded in rendering context. Just use them by referring as $this->html and $this->form.

Go to your browser and point it to http://localhost/lithium/posts and you should see a list of posts with pagination.

Adding new post

We will now proceed with adding a new post entry for our blog. In that process, we will also see how to add validations, use custom validations and use of save filter.

Open your PostControllers.php and add the following method to it after the existing index method.

PHP:
  1. public function add() {
  2.         if ($this->request->data) {
  3.             // Create a post object and add the posted data to it
  4.             $post = Post::create($this->request->data);
  5.             if ($post->save()) {
  6.                 $this->redirect(array('action' => 'index'));
  7.             }
  8.         }
  9.  
  10.         if (empty($post)) {
  11.             // Create an empty post object for use in form helper in view
  12.             $post = Post::create();
  13.         }
  14.  
  15.         $title = 'Add post';
  16.  
  17.         return compact('post', 'title');
  18.     }

The POST data is made available to us through $this->request->data. We are simply checking whether the post data is available and if so save it by calling the save() method. Before the data gets saved in the database, I would like my data to be validated. For this, I first need to specify the validation rules in the Post model. So now, open the model file Post.php and add the following just after the class definition

PHP:
  1. public $validates = array(
  2.                              'title' => array(
  3.                                          array('notEmpty', 'message' => 'Title cannot be empty'),
  4.                                          array('isUniqueTitle', 'message' => 'Title must be unique'),
  5.                                         ),
  6.                              'body' => 'Please enter some content for this post',
  7.                             );

Here, notEmpty is the built-in validation rule and Lithium will check whether some title is entered or not. But isUniqueTitle is a custom validation rule as I want my post titles unique. Being a custom validation rule, I must write the implementation for this rule as well. Add the following code to __init() method just after count finder.

PHP:
  1. Validator::add('isUniqueTitle', function ($value, $format, $options) {
  2.             $conditions = array('title' => $value);
  3.            
  4.             // If editing the post, skip the current psot
  5.             if (isset($options['values']['id'])) {
  6.                 $conditions[] = 'id != ' . $options['values']['id'];
  7.             }
  8.  
  9.             // Lookup for posts with same title
  10.             return !Post::find('first', array('conditions' => $conditions));
  11.         });

I also want to update the created/modified fields according to the action I am performing. Add the following code in continuation to the above code

PHP:
  1. Post::applyFilter('save', function($self, $params, $chain) {
  2.             $post = $params['record'];
  3.  
  4.             if (!$post->id) {
  5.                 $post->created = date('Y-m-d H:i:s');
  6.             } else {
  7.                 $post->modified = date('Y-m-d H:i:s');
  8.             }
  9.  
  10.             $params['record'] = $post;
  11.  
  12.             return $chain->next($self, $params, $chain);
  13.         });

Nothing very special in the above code except the return statement. We are calling next filter in the chain to make sure that all the filters run correctly. And now the form to add the data for post. Create a new file in app/views/posts and name it add.html.php. Add the following content to it

PHP:
  1. <h2>Add new post</h2>
  2. <?php
  3. $this->form->config(array('templates' => array('error' => '<div class="error"{:options}>{:content}</div>')));
  4. ?>
  5. <?=$this->form->create($post); ?>
  6.     <?=$this->form->field('title');?>
  7.     <?=$this->form->field('body', array('type' => 'textarea', 'rows' => 10));?>
  8.     <?=$this->form->submit('Add Post'); ?>
  9. <?=$this->form->end(); ?>

Form helper displays the validation errors for every field on its own. It creates a DIV element below the form element but doesn't apply any CSS class to it. But Lithium provides us a way to override any of the default element templates. This is exactly what I have done on line 3 in above code. If any one of you know the better way please let me know, I will update my code accordingly.

Again, point your browser to http://localhost/lithium/posts/add. You should see a form to enter post. Play with it to test validations. Finally you should be able to save the data in database.

Editing the post

Next comes the edit post entry. Lets dive into the code directly. Add the following code to PostsController.php after add()

PHP:
  1. public function edit($id = null) {
  2.         $id = (int)$id;
  3.         $post = Post::find($id);
  4.  
  5.         if (empty($post)) {
  6.             $this->redirect(array('controller' => 'posts', 'action' => 'index'));
  7.         }
  8.  
  9.         if ($this->request->data) {
  10.             if ($post->save($this->request->data)) {
  11.                 $this->redirect(array('controller' => 'posts', 'action' => 'index'));
  12.             }
  13.         }
  14.  
  15.         $title = 'Edit post';
  16.  
  17.         return compact('post', 'title');
  18.     }

Simple, right? Just remember that Post::find() is must before we call save(). Otherwise it will always insert a new entry in database. The URL to access this page will be naturally http://localhost/lithium/posts/edit/1 where 1 is the id of the post to edit.

View is mostly similar to add page. Create a new file in app/views/posts and name it edit.html.php and add the following code to it

PHP:
  1. <?=$this->form->create($post, array('method' => 'post')); ?>
  2.     <?=$this->form->hidden('id'); ?>
  3.     <?=$this->form->field('title');?>
  4.     <?=$this->form->field('body', array('type' => 'textarea'));?>
  5.     <?=$this->form->submit('Add Post'); ?>
  6. <?=$this->form->end(); ?>

All the validation rules will be automatically applicable for edit as well. For our custom validation rule we have already taken care of edit functionality by checking for post id in data. So, no change required in model.

Lets now proceed to the final action of our post controller, delete. In PostsController.php add the following code after edit() action

PHP:
  1. public function delete($id = null) {
  2.         $id = (int)$id;
  3.         $post = Post::find($id);
  4.  
  5.         if (empty($post)) {
  6.             $this->redirect(array('controller' => 'posts', 'action' => 'index'));
  7.         }
  8.  
  9.         $post->delete();
  10.         $this->redirect(array('controller' => 'posts', 'action' => 'index'));
  11.  
  12.         return;
  13.     }

That's it. No view required for delete. The above code will check whether the post with given id really exists and if it does, deletes the post and redirects to index action.

The whole code used in this tutorial is available in Github repository. For the sake of better display, I have slightly modified the default layout and the style.css in webroot/css. You can check out those files from repository.

This is just the beginning. A lot more can be added to this like adding comments for posts. This will follow in the next part. Till then try out this tutorial and don't forget to share your experience with us.


About this entry