Kniblet Tutorial Part 4
From n² wiki
Alternate Output Formats
Now we have a very simple HTML version of an article we can look at a pattern for making the same data available in more machine readable formats. Fortunately Konstrukt and Moriarty make this very simple. We're going to use this simple URL pattern for controlling the output formats:
/articles/{article-name}/{format-name}
Based on what we did before, that pattern suggests that we need to look at the ArticleController's forward method. We have a choice here. We could forward to a new controller or we could simply inline the code. For simplicity we choose to inline it, remembering that we can always extract a new controller later:
function forward($name) { if ($name == 'rdf') { $uri = $this->url(); $store = new Store(STORE_URI); $meta = $store->get_metabox(); $desc = $meta->describe_to_simple_graph($uri); $response = new k_http_Response(200); $response->setHeader("Content-type", "application/rdf+xml"); $response->setContent($desc->to_rdfxml()); throw $response; } }
Here we've just copied the code for retrieving an RDF description of the article from the GET method and use Moriarty's SimpleGraph class to convert it to RDF/XML for output. To bypass Konstrukt's normal processing we create a new Response, set its content type and body and the throw it as an exception. Konstrukt uses the pattern to output the response directly to the client rather than accumulating content through the various parent controllers.
Now, when we go to http://kniblet.local/articles/adopting-a-stray-cat/rdf we get the raw RDF/XML returned. Not only does this start integrating our application into the Web of Linked Data, it's a handy debugging tool too!
We can easily extend this pattern for other common formats such as turtle and JSON. With a little refactoring, the code becomes:
function describe_article() { $uri = $this->url(); $store = new Store(STORE_URI); $meta = $store->get_metabox(); $desc = $meta->describe_to_simple_graph($uri); return $desc; } function generate_response($type, $content) { $response = new k_http_Response(200); $response->setHeader("Content-type", $type); $response->setContent($content); throw $response; } function forward($name) { if ($name == 'rdf') { $desc = $this->describe_article(); $this->generate_response("application/rdf+xml", $desc->to_rdfxml() ); } else if ($name == 'turtle') { $desc = $this->describe_article(); $this->generate_response("text/plain", $desc->to_turtle() ); } else if ($name == 'json') { $desc = $this->describe_article(); $this->generate_response("application/json", $desc->to_json() ); } else { $response = new k_http_Response(404); $response->setHeader("Content-type", "text/html"); $response->setContent("<html><head><title>404 Not Found</title></head><body><h1>404 Not Found</h1></body></html>"); throw $response; } } function GET() { $vars = Array('title'=>'', 'body'=>''); $uri = $this->url(); $desc = $this->describe_article(); $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); }
We've also added a 404 handler for unknown formats.
Editing an Article
Now we have the capability to have different views of an article, we're in a position to add an edit view. The following URI should return the same data wrapped up as an HTML form:
/articles/{article-name}/edit
So, following the pattern above, we can add another clause in our if/else:
else if ($name == 'edit') { $uri = $this->url(); $desc = $this->describe_article(); $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/articleedit.tpl.php", $vars); }
Once again, we describe the article and extract some of its properties into an array which we pass to the template. This is identical to the GET method apart from the name of the template so it's ripe for refactoring. Our template, articleedit.tpl.php, looks like this:
<h1>Editing Article</h1> <form action="<?php echo htmlspecialchars($this->url()); ?>" method="post"> <label for="title">Title:</label> <input type="text" name="title" id="title" size="80" value="<?php echo htmlspecialchars($title); ?>" /> <br /> <label for="body">Body:</label> <textarea name="body" id="body" rows="12" cols="100"><?php echo htmlspecialchars($body); ?></textarea> <br /> <input type="submit" name="save" id="save" value="Save" /> or <a href="<?php echo htmlspecialchars($this->url()); ?>">cancel</a> </form>
When we go to /articles/adopting-a-stray-cat/edit we're presented with an html form containing our article's title and body. There are a couple of things that might be considered unusual here. The first is the action of the form: it points to the URI of the article. We could have set it to another URI, but we already have a controller for articles and it's quite RESTful for the resource to manage its own state. In the same way our cancel operation is simply a link back to the original article. The minimum of fuss and code needed.
Before we move on to handling the results of the form POST we'll do a quick refactoring, to extract the common code between editing and viewing an article results in a new method get_vars_from_article:
function get_vars_from_article() { $vars = Array('title'=>'', 'body'=>''); $uri = $this->url(); $desc = $this->describe_article(); $vars['title'] = $desc->get_first_literal($uri, DC_TITLE, ''); $vars['body'] = $desc->get_first_literal($uri, 'http://schemas.talis.com/kniblet/body', ''); return $vars; }
Our GET method looks like this now:
function GET() { $vars = $this->get_vars_from_article(); return $this->render("templates/article.tpl.php", $vars); }
We can now add a POST method to accept the results of the form POST. Our first attempt looks like this:
function POST() { $title = $this->POST['title']; $body = $this->POST['body']; }
Changesets
But what should we do with the submitted data? We need to somehow update the RDF in our store to use these values instead of the existing ones. The Talis Platform uses a scheme called Changesets to provide this functionality. A changeset is a list of triples that should be removed from a graph and a list of triples that should be added in their place. A changeset can be POSTed to a store's metabox to change the RDF it holds. By way of example, suppose our article looked 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/bucket"> <dc:title>Mending a Leaky Bucket</dc:title> <k:body>With what should I mend it dear Liza?</k:body> </sioc:Post> </rdf:RDF>
but after editing using our form we wanted it to look 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/bucket"> <dc:title>Mending a Leaky Bucket</dc:title> <k:body>With a straw dear Henry</k:body> </sioc:Post> </rdf:RDF>
We can compare the two descriptions and break the changes into a list of triples that have been removed and a list of triples that have been added. The easiest way to do this is to look at the descriptions in N-Triples format. So, before we had:
<http://kniblet.com/articles/bucket> <http://purl.org/dc/elements/1.1/title> "Mending a Leaky Bucket" . <http://kniblet.com/articles/bucket> <http://schemas.talis.com/kniblet/body> "With what should I mend it dear Liza?" .
and we ended up with:
<http://kniblet.com/articles/bucket> <http://purl.org/dc/elements/1.1/title> "Mending a Leaky Bucket" . <http://kniblet.com/articles/bucket> <http://schemas.talis.com/kniblet/body> "With a straw dear Henry" .
So to get from the former to the latter we need to remove the following:
<http://kniblet.com/articles/bucket> <http://schemas.talis.com/kniblet/body> "With what should I mend it dear Liza?" .
and add the following:
<http://kniblet.com/articles/bucket> <http://schemas.talis.com/kniblet/body> "With a straw dear Henry" .
The changeset specification provides an RDF vocabulary for indicating removals and additions. The previous two triples can be encoded like this:
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:cs="http://purl.org/vocab/changeset/schema#"> <cs:ChangeSet> <cs:subjectOfChange rdf:resource="http://kniblet.com/articles/bucket"/> <cs:createdDate>2008-04-14T10:52:00Z</cs:createdDate> <cs:creatorName>Kniblet User</cs:creatorName> <cs:changeReason>Change the body</cs:changeReason> <cs:removal> <rdf:Statement> <rdf:subject rdf:resource="http://kniblet.com/articles/bucket"/> <rdf:predicate rdf:resource="http://schemas.talis.com/kniblet/body"/> <rdf:object>With what should I mend it dear Liza?</rdf:object> </rdf:Statement> </cs:removal> <cs:addition> <rdf:Statement> <rdf:subject rdf:resource="http://kniblet.com/articles/bucket"/> <rdf:predicate rdf:resource="http://schemas.talis.com/kniblet/body"/> <rdf:object>With a straw dear Henry</rdf:object> </rdf:Statement> </cs:addition> </cs:ChangeSet> </rdf:RDF>
We can simply POST this RDF/XML to the store's metabox and it will replace the first triple by the second. There are a couple of things to note:
- the changeset needs to be POSTed with a content type header of vnd.talis.changeset+xml otherwise the store will assume it is simple more RDF to be merged into the metabox rather than a change to existing RDF
- the changeset will be rejected unless all the removal triples exist in the metabox. This can be used to guard against changes in a multiuser situation: the changeset only gets applied if the specified removal triples are definitely in the store.
A changeset has a number of properties that must be specified (the precise reasons why will become apparent later in the tutorial):
- subjectOfChange
- createdDate
- creatorName
- changeReason
As you might expect, Moriarty has a convenience class for assisting with changesets. It provides a class called Changeset which works out all the ins-and-outs of determining the difference between two descriptions, as well as serialising to RDF/XML. (In fact since it derives from SimpleGraph you can serialise as any supported format such as Turtle or JSON). Assuming the before and after descriptions of http://example.com/thing are held in $before_xml and $after_xml then you could create a changeset representing the differences between the descriptions like this:
$changeset = new Changeset(array( 'subjectOfChange'=> 'http://example.com/thing', 'before_rdfxml' => $before_xml, 'after_rdfxml' => $after_xml, 'creatorName' => 'Annie Doe', 'changeReason' => 'Need to fix this record', 'createdDate' => '2008-02-03T15:36:17+00:00, ));
Then you would apply it to the store's metabox like this:
$store = new Store(STORE_URI); $meta = $store->get_metabox(); $meta->apply_changeset( $changeset );
However, most stores metaboxes are protected for writes. This means that a username and password ought to be supplied when modifying RDF in the metabox via a changeset. The Talis Platform uses HTTP Digest for its authentication and Moriarty supports this automatically. You need to tell it what username and password to use by supplying a Credential object to the store. The Store object takes care of passing it to other resources and services.
$store = new Store(STORE_URI, new Credentials("user", "password")); $meta = $store->get_metabox(); $meta->apply_changeset( $changeset );
Now, we know how to get the current description of the article: it's just a call to the metabox's describe method. But we also need the current description, as specified by the data the user just submitted from our form. We can do this very easily by using Moriarty's SimpleGraph class. This class, as might be expected from its name, represents a very simple view of an RDF graph. It provides some useful methods to make manipulating graphs in PHP much simpler. The two we are interested in are add_resource_triple which adds a triple with a resource object, and add_literal_triple which adds a triple that has a literal for its object.
We need to create a new SimpleGraph, add the rdf:type for our article and both the title and body like this:
$new = new SimpleGraph(); $new->add_resource_triple( $uri, RDF_TYPE, 'http://rdfs.org/sioc/ns#Post' ); $new->add_literal_triple( $uri, DC_TITLE, $title ); $new->add_literal_triple( $uri, 'http://schemas.talis.com/kniblet/body', $body );
Putting this all together means our POST method ends up looking like this:
function POST() { $title = $this->POST['title']; $body = $this->POST['body']; $uri = $this->url(); $store = new Store(STORE_URI); $meta = $store->get_metabox(); $original = $meta->describe_to_simple_graph($uri); $new = new SimpleGraph(); $new->add_resource_triple( $uri, RDF_TYPE, 'http://rdfs.org/sioc/ns#Post' ); $new->add_literal_triple( $uri, DC_TITLE, $title ); $body = trim($body); $new->add_literal_triple( $this->uri, 'http://schemas.talis.com/kniblet/body', addcslashes($body , "\n\t\r")); $changeset = new Changeset(array( 'subjectOfChange'=>$uri, 'before_rdfxml' => $original->to_rdfxml(), 'after_rdfxml' => $new->to_rdfxml(), 'creatorName' => 'Anonymous Kniblet user', 'changeReason' => 'Anonymous edit', 'createdDate' => gmdate(DATE_W3C, time()), )); $store = new Store(STORE_URI, new Credentials("user", "password")); $meta = $store->get_metabox(); $response = $meta->apply_changeset($changeset); $redirect = new k_http_Response(303); $redirect->setHeader("Location", $uri); throw $redirect; }
This method follows this general pattern:
- get the title and body from the submitted form data
- fetch the current description of the article from the Platform store
- create a new version of the description
- create a changeset of the differences between the two descriptions
- submit the changeset back to the store
- redirect to the article URL
There are couple of gotchas that we're handling here:
- Our body property is expected to contain multiline text. Because the transport of the RDF literals is via an XML document protocol we run the risk of newlines and carriage returns being mangled by the various XML processors. To avoid this we use the addcslashes method to encode line endings and tabs. We need to remember to use stripcslashes when we want to display the text as it was entered.
- You might have spotted that we're actually creating two instances of Store: one without credentials and one with. That's due to a problem in some versions of PHP's cURL library that results in credentials being sent even when they are not needed. This sometimes results in the describe failing meaning that the changeset consists only of additions and no removals.
We haven't addressed any error handling yet, but we would need to check the response we get back from the changeset submission. This response is a standard HTTP response so we can check whether the operation succeeded by using the is_success() method. If the submission failed then we should redisplay the edit template and show the error message we received from the Platform.
Summary
- Exceptions break the normal flow of a Konstruct application
- In REST resources manage their own state
- Changesets are a way of representing differences between two descriptions of a resource
- Changesets consist of a list of triples to be removed and a list of triples to be added
- Store metaboxes accept POSTed changesets to modify their triples
- Changesets are only applied if all the triples to be removed actually exist in the graph
- The Platform uses HTTP digest authentication for secured operations

