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.