Rabbit

PHP is fun again
In this document, we will create a simple blog application using Rabbit. It will allow people to create blog posts. It will also allow other users to leave comments on each post, and everyone to view existing posts and all comments on them.

Installing Rabbit

We'll start by downloading Rabbit and installing it on our htdocs directory. Let's assume we're running Apache 2.2 and our htdocs is located at /var/www/example.org/htdocs, like in a lot of web servers. We'll extract Rabbit to /var/www/example.org/htdocs/blog so that the file /var/www/example.org/htdocs/blog/index.php is present, along with a number of other folders and directories. We'll assume our blog is accessible from the address http://www.example.org/blog.

Setting up the database

Fire up your favorite database administration utility, such as Baboo or phpMyAdmin, and create a new database named 'blog' with username 'barney' and password 'flindstone'. In it, we'll create two database tables, one to hold our blog posts, and one to hold the comments. For simplicity, assume in this tutorial that all users can post both posts and comments.

Here goes:

CREATE TABLE `posts` (
    `post_id` INT AUTOINCREMENT,
    `post_author` VARCHAR( 32 ),
    `post_title` TEXT,
    `post_text` TEXT,
    `post_created` DATETIME,
    PRIMARY KEY `post_id`
);


CREATE TABLE `comments` (
    `comment_id` INT AUTOINCREMENT,
    `comment_author` VARCHAR( 32 ),
    `comment_postid` INT,
    `comment_created` DATETIME,
    `comment_text` TEXT,
    PRIMARY KEY `comment_id`
);


As you can see, we're following the rabbit convention in naming our database fields by starting with the table name in singular followed by an underscore (you can get over that if you wish, with some modifications).

Settings

We'll have to write a few lines into the settings.php file. Most of the settings.php file is already written for us. We'll have to modify a few values:

<?php
    return array(
        'applicationname' => 'Blog',
        'rootdir'         => '/var/www/example.org/htdocs/blog',
        'production'      => false,
        'hostname'        => 'www.example.org',
        'url'             => '/blog',
        'port'            => 80,
        'webaddress'      => 'https://www.example.org/blog',
        'timezone'        => 'UTC',
        'language'        => 'en',
        'databases'       => array(
            'db' => array(
                'name'     => 'blog',
                'driver'   => 'mysql',
                'hostname' => 'localhost',
                'username' => 'barney',
                'password' => 'flindstone',
                'charset'  => 'DEFAULT',
                'tables'   => array( 'posts', 'comments' )
            )
        )
    );
?>


Testing

Let's test the connection to our database. Fire up your browser to http://www.example.org/blog to see if there are any errors. If you see a rabbit welcome message, we're good to move on.

Creating our libraries

We'll create two libraries, one per database table. Each library allows the frontend to use a storage engine (the database) which also encapsulates the application logic, allowing it to focus on presentation. We'll put each library in a separate file. Libraries are stored in the libs subfolder of your application root. Usually, each library contains a lot of logic, but in our case we can rely on the default behavior. A normal application will usually override this behavior.

Create two files, post.php and comment.php in your libs directory:

The Post library

libs/post.php:
<?php
    class Post extends Satori {
        protected $mDbTableAlias = 'posts';
        
        protected function Relations() {
            $this->Comments = $this->HasMany( 'CommentFinder', 'FindByPost', $this );
        }
    }
?>


The base class Satori offers the basic functionality -- communication with the database. It'll fetch the data from the database, and expose an object that follows the active record pattern. If you don't like this, or if you would like to follow a different pattern, you don't have to extend Satori, or you can override many functionalities it provides. For now, we'll consider it suffices. We have defined the database table that will be used to store our data in the mDbTableAlias variable. Satori uses this to determine where to store the data at and where to retrieve it from. All classes that extend Satori or offer similar functionality by communicating with the storage engine are called models.

The Relations() function is a special Satori function which allows us to specify public class attributes that represent relations between other models. In our case, the Comment class. We've specified that each Post has many comments. The CommentFinder and FindByPost identifiers that are used here will be created soon.

Let's now create a finder, a class which will allow us to retrieve all the posts in chronological order.

libs/post.php:
<?php
    class PostFinder extends Finder {
        protected $mModel = 'Post';
        
        public function FindAll( $offset = 0, $limit = 25 ) {
            return $this->FindByPrototype( New Post(), $offset, $limit, array( 'Created', 'DESC' ) );
        }
    }
?>


The base class Finder offers some base functionality that we can use when defining finders. We define the mModel attribute to contain the class name of our relevant model.

The FindAll() method uses the FindByPrototype() method of the base class that allows to create a finder easily and returns its results. The FindByPrototype() method retrieves records from the database based on certain criteria. By passing a brand new object as its first parameter, we're saying we don't want to filter based on anything -- i.e. we want to get all records. The second and third parameters are the offset and limit. Finally, the last parameter allows us to specify the order in which we want to fetch our records; in our case, we want to order by Created, in descending order.

We'll use this finder method to retrieve the posts we'll display.

The Comment Library

Finally, let us create the comments library.

libs/comment.php:
<?php
    class CommentFinder extends Finder {
        public function FindByPost( Post $post ) {
            $prototype = New Comment();
            $prototype->Postid = $post->Id;
            
            return $this->FindByPrototype( $prototype, 0, 250, array( 'Created', 'DESC' ) );
        }
    }
    
    class Comment extends Satori {
        protected $mDbTableAlias = 'comments';
        
        protected function Relations() {
            $this->Post = $this->HasOne( 'Post', 'Postid' );
        }
    }
?>


Here we create a simple model. The finder we have defined will allow us to find comments based on a post. If you recall, we have already specified a relation between Post and Comment in the Post model that will utilize this finder. Lookup the relation to verify that it uses this very finder that we just defined. The finding method accepts the post that we're interested in, hence it is passed $this in the relation defined in the Post model. Notice that we're also only displaying the latest 250 comments (this could be the basis for a future pagination system for your comments). We here use the FindByPrototype() functionality to search for records based on specific criteria -- a postid.

Notice how relations apply both ways: The Comment object has one Post, while the Post object has many Comments. The Postid parameter passed last to the HasOne relation definion is the attribute of the local object (Comment) used to determine which post it is linked to.

Loading Libraries

Now that we have created our libraries, we need to load them. Since this is a small project, we'll load all libraries on startup. Modify the file libs/project to set which libraries should be loaded on startup. We'll define the function Project_Construct(). Rabbit calls this function before each rendering session.

libs/project.php:
<?php
    function Project_Construct() {
        global $libs;
        
        $libs->Load( 'post' );
        $libs->Load( 'comment' );
    }
?>


The global singleton $libs is used to handle library loading. The Load method loads a library into memory.

Creating elements

Rabbit, unlike most frameworks, uses small structural bricks to construct the final page displayed to the user. These bricks are tiny bits and pieces that are rendered individually to form the target view. Each element contains very fundamental presentation logic.

Elements are separated by context and are split into directories and subdirectories. Each element is stored in its own file. All elements reside in the elements directory of your application root.

We'll start by creating an element that displays a single post and a single comment. We'll put elements relevant to posts in the elements/post directory and elements relevant to comments in the elements/comment directory.

Each element must consist of a function, the rendering function. It can accept arguments relevant to its presentation part. For instance, the element that displays a post will accept a Post instance as a parameter, while the element that displays a comment will accept a Comment instance as a parameter. The function name must start with the word 'Element'. It must be named after the element path if slashes are removed. For example, the element that resides at elements/post/view.php must contain a function named ElementPostView.

Displaying a post

elements/post/view.php:
<?php
    function ElementPostView( Post $post ) {
        ?><h2><?php
        echo htmlspecialchars( $post->Title );
        ?></h2>
        <p>by <strong><?php
        echo htmlspecialchars( $post->Author );
        ?></strong> on <?php
        echo $post->Created;
        ?></p><blockquote><?php
        echo htmlspecialchars( $post->Text );
        ?></blockquote><?php
    }
?>


This element takes a Post as a parameter and outputs its title, author, date, and text. Notice how conveniently all the post attributes have been made available to us by the default behavior of our model.

Displaying a comment

elements/comment/view.php:
<?php
    function ElementCommentView( Comment $comment ) {
        ?><p><strong><?php
        echo htmlspecialchars( $comment->Author );
        ?><strong> said...</p><blockquote><?php
        echo htmlspecialchars( $comment->Text );
        ?></blockquote><?php
    }
?>


Similarly, this element shows a single comment.

Listing comments

Let's now create an element that lists all the comments on a post.

elements/comment/list.php:
<?php
    function ElementCommentList( Post $post ) {
        if ( empty( $post->Comments ) ) {
            ?><p>No comments have been made on this post. <a href="comment?postid=<?php
            echo $post->Id;
            ?>">Add a comment</a>.</p><?php
            return;
        }
        ?><h3><?php
        echo count( $post->Comments );
        ?> comments on this post</h3><?php
        foreach ( $post->Comments as $comment ) {
            Element( 'comment/view', $comment );
        }
    }
?>


This element checks if there are any comments on the post and displays them if there are. Displaying comments is done by retrieving them from the model using the Comments attribute. Since this is a HasMany relation attribute, it will contain an array of Comment instances that have been posted on the Post.

We here use the Element() function to render an element within our element. We're passing it the $comment variable as an argument.

Listing posts

Let's finally create an element that lists all posts.

elements/post/list.php:
<?php
    function ElementPostList() {
        $finder = New PostFinder();
        $posts = $finder->FindAll();
        
        if ( empty( $posts ) ) {
            ?><p>No posts have been made yet. <a href="post">Make a post</a>.</p><?php
            return;
        }
        foreach ( $posts as $post ) {
            Element( 'post/view', $post );
            Element( 'comment/list', $post )
        }
        ?><p><a href="post">Add a new post</a>.</p><?php
    }
?>


Exporting elements

An element may call another, but someone has to call the first element. Some elements can be directly "called" by the end-user. The elements are attached to a URL and are called "master elements". Master Elements cannot be called from within other elements. When we make an element master, we say that we export it. Exporting is done by simply adding a line in the libs/project file, to the Project_PagesMap() function. We'll add a link to our post list to make it public. We want it to be accessed when someone loads the frontpage, so we'll attach it to the empty URL. These links between URLs and master elements are also called routes.

libs/project.php:
<?php
    function Project_PagesMap() {
        // routes to master elements
    	return array(
    		"" => "post/list", // here's what we added
    	);
    }
?>


Notice that there are a few more routes defined for development reasons. You can remove them if you don't want to use them. They allow for debugging and unit testing and are removed automatically when you put your project into production. We recommend that you keep them.

Trying it out

Simply go to http://www.example.org/blog/ and see what happens. The post/list master element should be called. You should be seeing an empty post list. We can create a post and two comments manually to try out our new system:

INSERT INTO `posts` (`post_author`, `post_title`, `post_text`, `post_created`)
VALUES              ('dionyziz', 'Test post!', 'I like it! :-)', NOW() - INTERVAL 2 HOUR);


INSERT INTO `comments` (`comment_postid`, `comment_author`, `comment_text`, `comment_created`)
VALUES                 ('1', 'abresas', 'This is a test comment on your post', NOW() - INTERVAL 5 MINUTE);


INSERT INTO `comments` (`comment_postid`, `comment_author`, `comment_text`, `comment_created`)
VALUES                 ('1', 'izual', 'This is another test comment!', NOW());


You can now go to http://www.example.org/blog/ to see if it'll display the post and the comments.

Writting posts

Let us now create the GUI that will allow people to write posts. We'll create a simple forms first:

elements/post/new.php:
<?php
    function ElementPostNew() {
        ?><h2>Create a new post</h2>
        <form action="do/post/new" method="post">
            Your name: <input type="text" name="author" /><br />
            Title: <input type="text" name="title" /><br />
            Text: <br />
            <textarea cols="30" rows="12" name="text"></textarea>
        </form><?php
    }
?>


We'll need to export this. Let's use the URL that we used earlier in our link.

libs/project.php:
<?php
    function Project_PagesMap() {
    	return array(
            ""     => "post/list", 
            "post" => "post/new",
			"comment" => "comment/new"
    	);
    }
?>


We've also exported the URL for the comment creation master element that we'll write up in a bit.

Creating actions

Finally, we need to define what happens when someone submits the form. Notice the do/post/new URL? This is automatically routed to a file called an action which must be created in the "actions" dirctory. Actions routes are identical to their paths, i.e. the URL and the path matches. Much like elements, they contain a single function named after the file path and can accept arbitrary arguments.

Let us create an action for handling post writting. Actions are separated into directories and subdirectories just like elements. We'll put the actions relevant to posts in the actions/post directory.

action/post/new.php:
<?php
    function ActionPostNew( tString $title, tString $text, tString $author ) {
        $title = $title->Get();
        $text = $text->Get();
        $author = $author->Get();
        
        if ( empty( $title ) || empty( $text ) ) {
            return Redirect();
        }
        if ( empty( $author ) ) {
            $author = 'anonymous';
        }
        
        $post = New Post();
		$post->Title = $title;
		$post->Text = $text;
        $post->Save();
        
        return Redirect();
    }
?>


This function accepts three arguments, the ones that also exist in our form. Notice that the names of the arguments and the form input fields match; this is singificant. These arguments are associated by Rabbit to the fields passed using HTTP POST. While order doesn't matter, names do.

The first thing we do is retrieval. This is done by calling the Get() method on each of the arguments. This is done because the user values cannot be trusted and must be type-validated. This might seem unreasonable now that we only have strings, but is of utter importance when integers or booleans are passed.

The named passing of arguments and type validation is a core idea in Rabbit and is called type-safety. It's completely different from the type validation that PHP offers on its own, both in functionality and implementation.

After the values have been retrieved, we check for an empty title or text and abort if we find any.

If everything is fine, we create a new post, add the data we want to it, and store it to the database.

Make a post!

Try clicking the add a post link to make your own post. Notice how posts are displayed in chronological order.

Writting comments

To create the GUI for comment posting, we'll create a similar master element that will contain the form to be used. Notice that at this point we want our master element to accept an argument by the user: which post they want to write the comment on. We'll use type-safety to retrieve that postid.

elements/comment/new.php:
<?php
    function ElementCommentNew( tInteger $postid ) {
        $postid = $postid->Get();
        $post = New Post( $postid );
        
        if ( !$post->Exists() ) {
            return Redirect();
        }
        
        ?><h2>Write a comment on <?php
        echo htmlspecialchars( $post->Title );
        ?></h2>
        <form action="do/comment/new" method="post">
            Your name: <input type="text" name="author" /><br />
            Text: <br />
            <textarea cols="30" rows="12" name="text"></textarea>
            <input type="hidden" name="postid" value="<?php
            echo $postid;
            ?>" />
        </form><?php
    }
?>


After we retrieve the postid, we check to see if the post that the user wants to comment on actually exists. If it does, we display its name along with the form; else, we redirect the user back to the post list. We also pass the postid along as a hidden field, since we'll need to use it to create the comment later.

Finally, we need to write an action to handle comment posting:

actions/comment/new.php
<?php
    function ActionCommentNew( tInteger $postid, tString $author, tString $text ) {
        $postid = $postid->Get();
        $author = $author->Get();
        $text = $text->Get();
        
        $post = New Post( $postid );
        if ( !$post->Exists() || empty( $text ) ) {
            return Redirect();
        }
        
        if ( empty( $author ) ) {
            $author = 'anonymous';
        }
        
        $comment = New Comment();
        $comment->Postid = $postid;
        $comment->Author = $author;
        $comment->Text = $text;
        $comment->Save();
        
        return Redirect();
    }
?>


Here, we retrieve the arguments passed to us. Again we check that the post passed exists and that the comment is not empty. Finally, we create the actual comment, and redirect the user back to where they were.

Conclusion

This concludes the simple blog tutorial. As you may have figured, Rabbit is not as simple as most frameworks out there, but exposes an extensible structure that "makes sense" for projects of any size -- from very small to very large. The logic is easy to understand once exposed to it, although it requires a certain learning curve to be able to develop in it. Thank you for reading; we hope you enjoyed your experience with Rabbit so far. If you have more questions, feel free to join us on #rabbit at Freenode.

^ Back to top