Getting started with lexa-tools: Blog in 15 Minutes

In this article, you will learn how to build data driven websites using the lexa-tools framework.

Step 1: Environment setup

Install the Apache web server with PHP 5.3, and enable mod_rewrite. Check your PHP configuration by running this script.

Install the NetBeans IDE with PHP support.

Download the lexa-tools framework from get-tools.amartynov.ru and unpack it. I will use the C:\lexa-tools path. Linux and Mac users should probably use their home folder.

Step 2: The site skeleton

Open NetBeans and create a new PHP project. For convenience, place it somewhere under the Apache web root, I will use the /blog/ path. Make sure to select PHP Version 5.3 in the project creation wizard. After the project is created, delete the auto-generated index.php file.

Add the lexa-tools files to the Include Path: right click on the Include Path tree node, select Properties, click Add Folder and select the lib directory under the lexa-tools location.

The Include Path will be used for code assistance (autocomplete, etc).

Create a folder named www. In production, it will be the root web directory. According to your hosting setup it might be better to name it public or htdocs. The name doesn’t matter.

Create the file www/.htaccess. Use this one. It’s quite a standard .htaccess which is used by virtually any site with human-readable URLs.

Create www/index.php. It will be the site entry point. For now, index.php will only contain one line — inclusion of the library:

require "c:/lexa-tools/lib/all.php";

Step 3: Blog post model

Being cool developers, we must first think of our data model. The most obvious entity in any blog is a Post.

Now it’s time to use the bundled lexa-tools CRUD interface generator.

Add a new XML file manage-config.xml to our project. Then replace its contents with the following:

<?xml version="1.0"?>
<config 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:noNamespaceSchemaLocation="http://tools.amartynov.ru/manage-schema">   
 
</config>

As you can see, we will use an XML schema and it will hint us about available tags and attributes. Use the Ctrl + Space shortcut to invoke the completion window:

Return to index.php and add these lines:

lexa_manage_register("manage", "../manage-config.xml");    
lexa_dispatch();

The first line tells that under the manage URL there will be a special admin interface. The second line starts the request processing.

Open the web browser and navigate to the manage url (in my case, to http://localhost/blog/www/manage). An authentication window should appear.

But no one can login because we haven’t created any logins. We are going to fix it. Add the following tag to the manage-config.xml file:

<auth login="admin" salt="salt" hash="b305cadbb3bce54f3aa59c64fec00dea" />

The contents of the hash attribute is calculated as md5($password . $salt). In this particular case, $password = "password" and $salt = "salt".

After this is done, you may login using the user name «admin» and password «password»:

The page is almost empty. Our task is to fill it with something interesting.

But first, we must connect to a database. The lexa-tools framework uses the orange-bean ORM for data operations. It’s a very simple, lightweight and agile ORM. To setup a database connection just add one line of code to index.php:

require "c:/lexa-tools/lib/all.php";
R::setup("sqlite:../database"); // HERE

We will use a SQLite database stored in the «database» file outside our www folder. Linux users may get into traditional troubles with write permissions, but I hope they know how to resolve such issues.

Ok, add more tags to manage-config.xml:

<group title="Blog">
    <entity name="post">            
        <text name="title" />
        <memo name="body" />
    </entity>
</group>

To understand their purpose refresh the page in the browser and click the «Post» link:

The orange message warns you that there’s not table «post» yet. Don’t panic, orange-bean will create it for you. So go ahead and click the «New» button. Write a couple of blog posts:

You haven’t written a line of code, but you are ready to populate your site with data!

Now try to save a blog post with all fields left empty. It will be saved successfully, but it shouldn’t. To fix this situation, let’s add some validation logic.

Create a file models.php:

use Lexa\Tools\Model;
 
class Model_Post extends Model {
 
}

And don’t forget to include it in index.php:

require "c:/lexa-tools/lib/all.php";
require "../models.php"; // ADD THIS LINE
R::setup("sqlite:../database"); 

orange-bean will use the class Model_Post for all beans (data objects) of kind «post». The class name is dictated by the orange-bean’s default model foramatter.

Return to models.php, put the cursor between the braces and press Ctrl + Space:

NetBeans is cool, isn’t it? Select the validate() method from the drop-down list and hit Enter. In the method body type $this->v and press Ctrl + Space again:

Complete the method as follows:

protected function validate() {
    $this->validate_presence("title");
    $this->validate_presence("body");
}

The code is self-explanatory: we want all posts to have title and body fields filled.

Now, what will happen when you try to save an empty post?

Good. But I forgot something. A blog post usually has a date of its publication. No problem. Switch to manage-config.xml and add:

<!-- add sort-default attribute -->
<entity name="post" sort-default="date desc">
    <!-- add this tag -->
    <date name="date" />
    <text name="title" />
    <memo name="body" />
</entity>

We added one more field (date) and told that we want posts to be ordered by date descending.

Also, let’s save us from having to manually specify the date for every new post:

class Model_Post extends Model {
 
    public function after_dispense() {
        $this->date = time();
    }       
 
    # ...
}

The after_dispense() method is called every time the Model_Post object is created by the orange-bean.

Please, don’t forget to set dates for two posts created earlier!

So, we completed the task. You have learned that the lexa-tools uses a very interesting ORM and has a powerful admin interface generator. These two core components allowed us to build a management console for our new site in minutes.

Now it’s time to show our blog to the public…

Step 4: Home page

Navigate to the site home page, http://localhost/blog/www/:

The lexa-tools dispatches requests according to the registered routes. Let’s register one. Go to index.php and add:

R::setup("sqlite:../database"); 
 
// THIS
lexa_route("/", function() {
    $posts = R::find("post", "order by date desc limit 10");
    print_r($posts);
}); 

We used the lexa_route function. It states: the URL «/» will be handled with this anonymous function. Refresh the page in the browser. The error is gone, but the output of the print_r function is not what we want to show to our visitor.

Replace the print_r call with this line:

print lexa_render("home", compact("posts"));

Then create a folder views and create the file views/home.php:

<h2>Recent posts</h2>
 
<? foreach($posts as $p): ?>
<div>
    <?= date("M d, Y", $p->date) ?>
</div>
<h3>
    <?= htmlspecialchars($p->title) ?>
</h3>
<div>
    <?= htmlspecialchars($p->body) ?>
</div>
<hr />
<? endforeach ?>

The page will look as follows:

That’s it. The home page is ready.

I haven’t said anything about MVC, but actually we used this concept. First, we created a Model, implicitly in manage-config.xml and then explicitly by introducing the Model_Post class. The anonymous function passed to the lexa_route function is our controller (the lexa-tools also supports true class-based controllers, but in our simple case we will keep things simple). The controller performed some actions (namely, fetched top 10 recent posts) and invoked a View to present this list to our public.

Of course, the public wants to write comments…

Step 5: Comments

In the manage-config.xml file, add one more entity to the Blog group:

<entity name="comment" sort-default="date desc">
    <filters>
        post_id
    </filters>
 
    <lookup name="post_id" title="Post" show-in-list="false" />
    <date name="date" time="true" />
    <text name="name" />
    <memo name="text" />
</entity>

An interesting thing here is the post_id column. It’s a lookup, it’s automatically wired to the post entity (by name), it’s hidden from the list, but used in the filter.

Return to the management console and click the Comment link. Something is wrong with the filter items:

The items «post (1)» and «post (2)» are a bit strange. Of course, there is a solution. Go to models.php and override the magic __toString() method of the Model_Post class:

public function __toString() {
    return date("Y/m/d", $this->date) . " - " . $this->title;
}

The list now looks more reasonable:

On the spot, add validation rules:

// append this to models.php
class Model_Comment extends Model {
 
    public function after_dispense() {
        $this->date = time();
    }
 
    protected function validate() {
        $this->validate_presence("post_id");
        $this->validate_presence("name");
        $this->validate_length("text", 3, 2000);
    }
 
}

And teach posts to fetch their comments:

class Model_Post extends Model {
 
    # ...
 
    function comments() {
        return R::find("comment", "where post_id = ? order by date desc", $this->id);
    }
 
}   

Add a couple of comments for the first blog post:

Step 6: Page for a Post

We need a separate page where we could display a single blog post together with its comments. For that, we must register another route:

// add to index.php
lexa_route("post-route", "post/:id", null, array("id" => "\d+"), function($r) {
    $post = R::load("post", (int)$r["id"]);
    if(!$post)
        lexa_http_error(404);
 
    print lexa_render("post", compact("post"));
});

Here the lexa_route function takes more arguments. The first argument is the route name, we will use it later to generate links. The second is the URL pattern, it has a dymanic segment :id. The third argument could be used to specify default values for route segments. The fourth parameter constrains the id segment to a number. The last argument is a handler, and it accepts the array $r which will contain a value for the id segment.

In the handler, we load the post from the database. If it isn’t found, then the 404 error will be thrown. Finally, we render the view.

Create a view file (views/post.php):

<h2><?= htmlspecialchars($post->title) ?></h2>
<div>
    <small><?= date("M d, Y", $post->date) ?></small>
</div>
<div>
    <?= htmlspecialchars($post->body) ?>
</div>
 
<h3>Comments</h3>
 
TODO

But nobody would reach this page if we haven’t put somewhere a link to it. In views/home.php add this code before the hr tag:

<div>
    <?= lexa_link_to("Permalink", lexa_route_url("post-route", array("id" => $p->id))) ?>
</div>

Step 7: Layout

We have alredy created two pages: the home page and the post page. Usually, different pages of the same site share same headers, footers, sidebars, menus and other common components.

Like in many frameworks, in lexa-tools, this is done using layouts.

Create a new file views/layout.php:

<!DOCTYPE html>
<html>
    <head>
        <title>My blog</title>
    </head>
    <body>
        <h1>My blog</h1>
        <hr />
 
        <?= lexa_yield() ?>
 
        Copyright &copy; me
    </body>
</html>

and at the very beginning of both views/home.php and views/post.php files add this line:

<? lexa_layout("layout") ?>

The site still looks ugly but at least it has a header and a footer:

Step 8: Partial view for comments

Another way of markup reuse is partial views. We will use them for comments.

Create a file views/comment.php:

<div>
    <?= htmlspecialchars($comment->name) ?>, <?= date("M d, Y h:i a", $comment->date) ?>
</div>
<div>
    <?= htmlspecialchars($comment->text) ?>
</div>
<hr />

And replace the «TODO» stub in views/post.php with the following piece of code:

<div id="comments">
<? 
    foreach($post->comments() as $comment) {
        print lexa_partial("comment", compact("comment"));
    }        
?>
</div>

It will result in:

Step 9: Comments form

The final step is to allow our readers to leave their comments. We’ll use jQuery.

Include jQuery in views/layout.php:

<title>My blog</title>
<script 
    type="text/javascript" 
    src="http://ajax.googleapis.com/ajax/libs/jquery/1.6/jquery.min.js">
</script>

Register the third route in index.php:

lexa_route("comment-route", "comment", function() {
    lexa_accept_verbs("post");
 
    $comment = R::dispense("comment");
    $comment->post_id = (int)$_POST["post_id"];
    $comment->name = $_POST["name"];
    $comment->text = $_POST["text"];
 
    R::store($comment);
 
    print lexa_render("comment", compact("comment"));
});  

Append more markup and code to views/post.php:

<div>
    <label for="name_field">Name:</label>
    <?= lexa_text_field_tag("name_field") ?>
</div>
<div>
    <?= lexa_text_area_tag("text_field", "") ?>
</div>
<div>
    <input id="send_button" type="button" value="Send" />
</div>
 
<script type="text/javascript">
    $("#send_button").click(function() {
 
        $.post(
            <?= json_encode(lexa_route_url("comment-route")) ?>,
            {
                post_id: <?= $post->id ?>,
                name: $("#name_field").val(),
                text: $("#text_field").val()
            },
            function(data) {
                $("#comments").prepend(data)
            }
        );
 
    });
</script>

Now you can write comments using the form at the bottom of the comment list!

Summary

We’ve covered the basics of the lexa-tools framework. The full source code of the created «blog engine» can be downloaded here.

To read more about lexa-tools and all its features, please visit this page.

Ваш комментарий