Kniblet Tutorial Part 5
From n² wiki
Creating Articles
Now we can edit articles and we have the basic update mechanism in place it shouldn't be too difficult to enable the creation of new ones. The first thing we must do is design the URL structure. Creating a new article is basically an operation on the collection of articles, i.e. an append operation. This suggests that a POST to the articles collection at /articles is the right way to handle it. We also need a way to access the create article form. We're going to follow a similar pattern as for the edit article form and use the following URL:
http://kniblet.com/articles/new
Last time we did this we added a new forward method to ArticleController to switch on the keyword edit. This is similar so we need to modify the forward method in our ArticleListController to recognise the new keyword:
function forward($name) { if ($name) { if ($name == 'new') { $vars = Array('title'=>'', 'body'=>''); return $this->render("templates/articleedit.tpl.php", $vars); } else { $next_controller = new ArticleController($this, $name); return $next_controller->handleRequest() ; } } else { return $this->GET(); } }
Note how we're reusing the article edit template. By passing in empty values for the template variables we're ensuring that we get an empty form. Also, because the form in that template is set up to POST back to the controller's URL it will automatically send the form data to /articles. Now we need to write a POST method for our controller. It needs to:
- generate a URI for the new article
- read the form data and convert to an RDF graph representing the new article
- create a changeset representing the additional triples we need to add
- post the changeset to the store's metabox
- redirect to view the new article
Steps 2 to 5 are basically the same as the steps we're performing in the POST method of the ArticleController class. In that class we're also fetching the existing article from the store to calculate the changeset. Here we know all our triples are additions. Aside from that the two methods do seem to be doing much the same thing. This sounds ripe for a refactoring and the commonality between the two controllers is that they are both dealing with articles. It seems the time is right to extract a new class to represent Articles. This class needs to deal with translating between form variables and RDF graphs as well as constructing appropriate changesets. We're going to leave the network code out in the controllers though because the logic for reporting errors will be different depending on whether we're creating a new article or editing an existing one.
So here's our new Article class:
class Article { var $graph; var $uri; function __construct($uri = null, $graph = null) { $this->uri = $uri; if (null == $graph) { $this->graph = new SimpleGraph(); } else { $this->graph = $graph; } } function get_vars() { $vars = Array('title'=>'', 'body'=>''); if ( null != $this->uri) { $vars['title'] = $this->graph->get_first_literal($this->uri, DC_TITLE, ''); $vars['body'] = stripcslashes($this->graph->get_first_literal($this->uri, 'http://schemas.talis.com/kniblet/body', '')); } return $vars; } function calculate_changeset($vars, $creator_name, $change_reason) { $new = new SimpleGraph(); if ( null != $this->uri) { $new->add_resource_triple( $this->uri, RDF_TYPE, 'http://rdfs.org/sioc/ns#Post' ); $new->add_literal_triple( $this->uri, DC_TITLE, $vars['title'] ); $body = trim($vars['body']); $new->add_literal_triple( $this->uri, 'http://schemas.talis.com/kniblet/body', addcslashes($body , "\n\t\r")); } $changeset = new Changeset(array( 'subjectOfChange'=>$this->uri, 'before_rdfxml' => $this->graph->to_rdfxml(), 'after_rdfxml' => $new->to_rdfxml(), 'creatorName' => $creator_name, 'changeReason' => $change_reason, 'createdDate' => gmdate(DATE_W3C, time()), )); return $changeset; } }
The constructor takes a URI and optionally a graph of RDF data representing the article. The URI can be null which represents a new, unnamed article. A get_vars method converts the triples in the article's graph into template variables compatible with our existing template. Finally, a calculate_changeset method takes a set of variables derived from form data, the name of the creator of the change and the reason for a change and generates a changeset based on any pre-existing RDF held for that article. All of this code already existed in one form or another in our ArticleController but they're now in a single coherent place. We can now modify our controllers to use this new class.
Our get_vars_from_article method now creates a new article seeded with RDF from the store and just calls its get_vars method. This insulates us from future problems when we introduce more properties for articles and we need to add more template variables.
function get_vars_from_article() { $uri = $this->url(); $desc = $this->describe_article(); $article = new Article($uri, $desc); return $article->get_vars(); }
The ArticleController's POST method gets simpler too:
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); $article = new Article($uri, $original); $changeset = $article->calculate_changeset( Array('title' => $title, 'body' => $body), 'Anonymous Kniblet user', 'Anonymous edit'); $store = new Store(STORE_URI, new Credentials("knibletapp", "rkgm5gqz")); $meta = $store->get_metabox(); $response = $meta->apply_changeset($changeset); $redirect = new k_http_Response(303); $redirect->setHeader("Location", $uri); throw $redirect; }
Now we are delegating the creation of the changeset to the Article class. We still apply it in the controller so we can handle errors and success events more easily.
We're in a position to write our ArticleListController's POST method. It will still be similar to the ArticleController's POST method but a lot of the essential duplication has now been consolidated into a single place: the Article class. Our POST method looks like this:
function POST() { $title = $this->POST['title']; $body = $this->POST['body']; $uri = 'http://' . $_SERVER["HTTP_HOST"] . '/articles/' . $this->make_slug($title, 28); $article = new Article($uri); $changeset = $article->calculate_changeset( Array('title' => $title, 'body' => $body), 'Anonymous Kniblet user', 'Anonymous edit'); $store = new Store(STORE_URI, new Credentials("knibletapp", "rkgm5gqz")); $meta = $store->get_metabox(); $response = $meta->apply_changeset($changeset); $redirect = new k_http_Response(303); $redirect->setHeader("Location", $uri); throw $redirect; }
We've also introduced a new method to create the URI of the new article: make_slug. This method simply takes the title of the article, removes some common words, eliminates punctuation, replaces spaces with hyphens and lowercases the result. For example A Guide To Opening Doors would become guide-opening-doors:
function make_slug($input, $max_length) { $stoplist = Array("a", "an", "and", "as", "at", "before", "but", "by", "for", "from", "is", "in", "into", "like", "of", "off", "on", "onto", "per", "since", "than", "the", "this", "that", "to", "up", "via", "with"); $input = preg_replace("/\\b(" . implode('|', $stoplist) . ")\\b/i", '', $input); $input = preg_replace("/[^-A-Z0-9\s]/i", '', $input); $input = trim($input); $input = preg_replace("/[\s]+/i", '-', $input); $input = strtolower($input); return substr($input, 0, $max_length); }
We also need to modify our article list template to include a link to the new article page. We can just add a link like this before the form:
<p><a href="/articles/new">Create a new article</a></p>
We should be able to view the article search page, use this link and create new articles in our store.
Simple Enhancements
We now have all the essentials in place for managing articles: creation, editing, searching and viewing in a variety of formats. We could add deleting too if we wanted to, which would be the submission of a changeset consisting solely of triples to remove. These basic interaction patterns can be used over and over again for other types of information in our system. We can also change the behaviour of our articles with very little effort. For example, suppose we wanted to integrate a text-based formatting language for article bodies such as Markdown (http://daringfireball.net/projects/markdown/). We could pick one of the open source implementations (such as this one: http://www.michelf.com/projects/php-markdown/) and use it whenever we render the body. For example we would modify the article template like this:
<?php require_once LIB_DIR . '/markdown/markdown.php'; $parser = new Markdown_Parser(); ?> <h1><?= $title ?> <a href="<?php echo htmlspecialchars($this->url()) . '/edit'; ?>" accesskey="e">[edit]</a></h1> <div class="content"><?php echo $parser->transform($body); ?></div>
Versioning
We're now going to look at another feature that the Platform provides which could add an extra dimension to our application. The changesets we have been using so far have been unversioned: we submit them, they take effect and then they disappear. However it is also possible to apply them in a versioned mode where after they've taken effect they are kept in the store as a kind of version history. To change from unversioned to versioned changesets we need to change from POSTing to
http://api.talis.com/stores/kniblet-dev1/meta
and POST to
http://api.talis.com/stores/kniblet-dev1/meta/changesets
instead. That's all there is to it. The changesets themselves remain the same, just the address they are POSTed to is different. Moriarty supports this automatically through the use of the submit_versioned_changeset method on the Metabox class. We just need to replace our call to submit_changeset in ArticleController's POST method:
$response = $meta->apply_versioned_changeset($changeset);
Now whenever we make a change to an article a record of that change is also kept in the store's metabox. We can use these stored changes to build a version history for an article. Getting this version history means querying the store with a little SPARQL. Each store provides a public SPARQL interface with an HTML form for experimentation:
http://api.talis.com/stores/kniblet-dev1/services/sparql
Now if we create an article and make a few edits we should be able to query for the changesets corresponding to those edits with the following SPARQL query:
prefix cs: <http://purl.org/vocab/changeset/schema#>
select ?cs ?creator ?date ?reason
where {
?cs cs:subjectOfChange <http://kniblet.local/articles/history-test> ;
cs:creatorName ?creator ;
cs:createdDate ?date ;
cs:changeReason ?reason .
}
order by desc(?date)
This query looks for all changesets that have a subject of change equal to our article's URI. For each changeset it selects the creator name, date and reason. Then we sort the results so that the most recent changes are first. We can use the results of this query to display a list of changes made to each article. First of all we need to fetch the changes in the ArticleController's GET method and pass the results into the template using the variable array:
function GET() { $vars = $this->get_vars_from_article(); $cs_query = "prefix cs: <http://purl.org/vocab/changeset/schema#> select ?cs ?creator ?date ?reason where { ?cs cs:subjectOfChange <" . $this->url() . "> ; cs:creatorName ?creator ; cs:createdDate ?date ; cs:changeReason ?reason . } order by DESC(?date)"; $store = new Store(STORE_URI); $sparql = $store->get_sparql_service(); $vars['history'] = $sparql->select_to_array($cs_query); return $this->render("templates/article.tpl.php", $vars); }
Here we're using Moriarty's select_to_array() method which executes the SPARQL query and parses the results into a PHP array structure. We now modify our template to display the history for the article by adding in this code just after we output the article body:
<?php if (count($history) > 0) { echo '<h2>Recent Changes</h2>'; echo '<ul>'; foreach ($history as $item) { echo '<li>'; $date = strtotime($item['date']['value']); echo htmlspecialchars(strftime("%R, %e %B %Y", $date)) . ' by ' . htmlspecialchars($item['creator']['value']) . ' (<em>' . htmlspecialchars($item['reason']['value']) . '</em>)'; echo '</li>'; } echo '</ul>'; } ?>
So now we have a simple display of who has been editing each article. We could improve it by allowing the users to enter a reason for each change. In articleedit.tpl.php we add a new form field:
<label for="reason">Reason for change:</label> <input type="text" name="reason" id="reason" size="80" value="" /> <br />
We now need to handle that in ArticleController's POST method. If the user omits it we replace it with a default reason. We can also be slightly more informative with regard to the creator of the change. We haven't built any user account functionality yet so we record the user's IP address.
$reason = $this->POST['reason']; if ( empty($reason) ) { $reason = "no reason"; } $user = "anonymous user at " . $_SERVER["REMOTE_ADDR"];
Now we can just pass those into the calculate_changeset method:
$changeset = $article->calculate_changeset( Array('title' => $title, 'body' => $body), $user, $reason);
When we make an edit we can now enter a reason and this will be displayed in the list of changes for each article along with some identifying information. Because the triples that were changed by each changeset are also stored in the metabox we could could also display the actual changes made and with a little additional effort we could even show side by side comparisons of versions.
Another extension would be the ability to revert changes. If we were reverting the most recent change for an article then it would simply be a matter of creating a new changeset and adding all the previous changeset's removals as additions and all its additions as removals, i.e. undoing the changes it made. To go back further we would need to gather all the removals and additions together, eliminate any removals and additions of the same triple and generate a new changeset with the remaining changes reversed.
Recent Changes
Our SPARQL query above can be modified slightly to list all the changesets in the store:
prefix cs: <http://purl.org/vocab/changeset/schema#>
select ?cs ?soc ?creator ?date ?reason
where {
?cs cs:subjectOfChange ?soc ;
cs:creatorName ?creator ;
cs:createdDate ?date ;
cs:changeReason ?reason .
}
order by desc(?date)
We'd like to use this to create a recent changes page for the whole application. This might be useful for returning users who want to keep up to date with what's been happening. We're going to put our recent changes list at:
http://kniblet.com/changes
The steps we need to follow are similar to those we took for managing articles. We need to:
- create a controller that fetches the recent changes
- create a template to display them
- hook the controller into the application
The display of changes will be similar but we also need to include the title of the article so the user knows what each change applies to. To avoid getting overwhelmed it's a good idea to put a limit on the number of results returned by the query. We choose 30 as a sensible number. With a little more effort we could introduce paging of results but for now we'll just display the most recent 30 changes. Our query now looks like the following:
prefix cs: <http://purl.org/vocab/changeset/schema#>
prefix dc: <http://purl.org/dc/elements/1.1/>
select ?cs ?soc ?creator ?date ?reason ?title
where {
?cs cs:subjectOfChange ?soc ;
cs:creatorName ?creator ;
cs:createdDate ?date ;
cs:changeReason ?reason .
?soc dc:title ?title .
}
order by desc(?date)
limit 30
The controller is very simple, consisting of just a GET method:
class ChangesController extends k_Controller { function GET() { $vars = array(); $cs_query = "prefix cs: <http://purl.org/vocab/changeset/schema#> prefix dc: <http://purl.org/dc/elements/1.1/> select ?cs ?soc ?creator ?date ?reason ?title where { ?cs cs:subjectOfChange ?soc ; cs:creatorName ?creator ; cs:createdDate ?date ; cs:changeReason ?reason . ?soc dc:title ?title . } order by desc(?date) limit 30"; $store = new Store(STORE_URI); $sparql = $store->get_sparql_service(); $vars['history'] = $sparql->select_to_array($cs_query); return $this->render("templates/changes.tpl.php", $vars); } }
And the corresponding changes.tpl.php template:
<h1>Recent Changes</h1> <?php if (count($history) > 0) { echo '<ul>'; foreach ($history as $item) { echo '<li>'; $date = strtotime($item['date']['value']); echo '<a href="' . htmlspecialchars($item['soc']['value']) . '">'; echo htmlspecialchars($item['title']['value']) . ': '; echo '</a>'; echo htmlspecialchars(strftime("%R, %e %B %Y", $date)) . ' by ' . htmlspecialchars($item['creator']['value']); echo ' (<em>' . htmlspecialchars($item['reason']['value']) . '</em>)'; echo '</li>'; } echo '</ul>'; } ?>
Finally, to hook it up into our application we need to modify the root controller's forward method:
function forward($name) { if ($name == 'articles') { $next_controller = new ArticleListController($this, $name); } else if ($name == 'changes') { $next_controller = new ChangesController($this, $name); } else { throw new k_http_Response(404); } $vars = Array('content' => $next_controller->handleRequest()); return $this->render("templates/root.tpl.php", $vars); }
We also modify our root template to link to the list of recent changes in the global navigation. Our application is starting to take shape!
Summary
- Templates can be reused in Konstrukt
- Enabling a list of resources to be added to via POST from a form
- Creating a new resource can be done with a changeset having no removals
- Changesets can be versioned or unversioned
- Versioned changesets are stored in the metabox
- Versioned changesets can be SPARQLed
- Use versioned changesets to provide version histories for resources

