Kniblet Tutorial Part 3
From n² wiki
Introducing Articles
The core of our application are the articles that the users create. We need to implement the following functions:
- Search for articles
- View an article
- Create a new article
- Edit an existing article
We also want our article to be accessible as raw RDF and allow users to tag and categorise them.
Bootstrapping the Data
One of the advantages of using RDF as the native data format is that we can bootstrap our system simply by loading RDF into the Platform Store. So, to get started we'll just create some RDF representing a Kniblet article like this:
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sioc="http://rdfs.org/sioc/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:k="http://schemas.talis.com/kniblet/" > <sioc:Post rdf:about="http://kniblet.com/articles/adopting-a-stray-cat"> <dc:title>Adopting A Stray Cat</dc:title> <k:body> It can be very rewarding to adopt a stray cat. Cats become stray for all sorts of reasons but often they have lost their original owner or have been driven away through cruelty. Before you can adopt a stray cat you must win its trust. If you regularly feed it and don't approach it too soon it may learn that you mean it no harm. Persevere, gradually gaining its confidence until you can finally touch it and even call it over for its food. </k:body> </sioc:Post> </rdf:RDF>
We can then just POST it straight into our store using the command line (or any other tool we have to hand):
curl -d @data.rdf --digest -u "user:password" -H content-type:application/rdf+xml http://api.talis.com/stores/kniblet-dev1/meta
We can check that it was loaded successfully by using the Store's Sparql service:
http://api.talis.com/stores/kniblet-dev1/services/sparql
and running the following simple query:
describe <http://kniblet.com/articles/adopting-a-stray-cat>
Finding Articles
Now we have an article in the store, we can work on allowing our users to discover articles without knowing their exact URL. We can do this quite simply by providing a search form that can be accessed at our /articles URL. To do this we need to create an ArticleList controller that intercepts requests containing /articles. The skeleton of the contoller looks like this with a single overridden GET method:
class ArticleListController extends k_Controller { function GET() { $vars = array( ); return $this->render("templates/articlelist.tpl.php", $vars); } }
We also need to create articlelist.tpl.php:
<form method="get" action=""> <label for="q">Search for an article:</label> <input type="text" name="q" size="30" /> <input type="submit" name="go" value="Search" /> </form>
When we test that page by going to /articles we're presented with a simple search form. Entering a search does nothing though, so we need to fix that.
We made our form use the GET method so we could allow searches to be bookmarked. So we need to add code to the controller's GET method to handle the form data.
Our store's URI is:
http://api.talis.com/stores/kniblet-dev1
We perform free text searches on a store by using it's contentbox:
http://api.talis.com/stores/kniblet-dev1/items
If we visit that URI we can see an HTML form that describes the parameters understood by the contentbox. The key one for our purposes is called query. If we type the word cat into the search box and click the search button we should get a search response containing a single response. This response is in RSS 1.0 format which has the dual advantages of being extremely widely understood by RSS toolkits but also parseable as RDF. We get the best of both worlds here: huge toolkit support for RSS but full RDF flexibility too.
A search for cat using that form takes us to a URL like:
http://api.talis.com/stores/kniblet-dev1/items?query=cat&max=10&offset=0&sort=&xsl=&content-type=
To build search into our application we just need to ensure that we construct a URL like that. All those parameters except query are optional so we could just as well use
http://api.talis.com/stores/kniblet-dev1/items?query=cat
We're going to use Moriarty to help build the search URLs and parse the responses, so we need to pull some classes in at the top of articlelistcontroller.php:
require_once KNIBLET_MORIARTY_DIR . '/constants.inc.php'; require_once KNIBLET_MORIARTY_DIR . '/store.class.php';
Now we'll check to see if the q parameter has been sent and, if so, perform the search against the kniblet platform store:
function GET() { $vars = array( 'results' => null, 'q' => '' ); if (isset($this->GET['q']) && ! empty($this->GET['q']) ) { $query = $this->GET['q']; $store = new Store('http://api.talis.com/stores/kniblet-dev1'); $cb = $store->get_contentbox(); $vars['results'] = $cb->search_to_resource_list($query); $vars['q'] = $query; } return $this->render("templates/articlelist.tpl.php", $vars); }
This shows a common pattern for Moriarty:
- Create a Store class
- Get the relevant service or resource
- Perform the action on the service or resource
Note how we use the $vars array to pass data from our PHP code to our template. In this case we're supplying two parameters:
- results
- contains the raw search results
- q
- contains the original query as supplied by the user
Now, in our template we want to display a list of any matching articles found:
<form method="get" action=""> <label for="q">Search for an article:</label> <input type="text" name="q" size="30" value="<?php echo htmlspecialchars($q); ?>"/> <input type="submit" name="go" value="Search" /> </form> <?php if ($results) { echo '<ul>'; foreach ($results->items as $result) { echo '<li>' . $result[DC_TITLE][0] . '</li>'; } echo '</ul>'; } ?>
As a nicety we've also made the user's original query appear in the search box after a search.
We can test that this is all working by searching for the word "cat". We now see a single article listed. However, we can't click on it or do anything to it. Our next task is to link it to a full display.
Viewing Articles
We want our articles to be accessible using the following URL pattern:
/articles/{article-name}
Currently the ArticleListController handles any requests to /articles. We need to modify it to detect additional characters after /articles and pass them onto a new ArticleController class. We can do that in by overriding the controller's forward method like this:
class ArticleListController extends k_Controller { function forward($name) { if ($name) { $next_controller = new ArticleController($this, $name); return $next_controller->handleRequest() ; } else { return $this->GET(); } } function GET() { $vars = array( ); return $this->render("templates/articlelist.tpl.php", $vars); } }
We also need to modify our article list template to insert the article link:
echo '<li><a href="' . htmlspecialchars($result[RSS_LINK][0]) . '">' . $result[DC_TITLE][0] . '</a></li>';
Our ArticleController class starts off looking similar to the ArticleListController:
class ArticleController extends k_Controller { function GET() { $vars = array( ); return $this->render("templates/article.tpl.php", $vars); } }
However, we need to obtain the RDF description of the article from our store and render it in the template. We can so this using the store's metabox:
function GET() { $vars = Array('title'=>'', 'body'=>''); $uri = $this->url(); $store = new Store(STORE_URI); $meta = $store->get_metabox(); $desc = $meta->describe_to_simple_graph($uri); $vars['title'] = $desc->get_first_literal($uri, DC_TITLE, ''); $vars['body'] = $desc->get_first_literal($uri, 'http://schemas.talis.com/kniblet/body', ''); return $this->render("templates/article.tpl.php", $vars); }
Our template is very simple:
<h1><?php echo htmlspecialchars($title) ?></h1> <div class="content"><?php echo htmlspecialchars($body); ?></div>
Summary
- RDF allows applications to boostrap their data very simply
- RDF is added to a store using HTTP POST to metabox
- Each store provides a SPARQL service to query its RDF
- A DESCRIBE query can be used to get an RDF description of a resource
- Stores can be free text searched using contentbox
- Moriarty uses a common pattern of creating a store object, getting a resource from it and then using it
- Moriarty provides simple ways of accessing the results of a DESCRIBE

