Extending Torii
Extending Torii mainly means writing new modules. To show you, how you could develop a module I will show how you could recreate the TODO list module, which provides a really simple example.
The basic structure
For a new module you should first create a new directory in the module folder of your Torii installation. The name of the folder is also the future identifier for your module. So, for the tutorial we first start creating a folder with the name "tutorial" in the module folder:
torii/module/tutorial/In this folder the main module class is expected in the file class.php which the torii framework will try to include. In this file a class is expected which is called "torii_Module<ModuleName>", in our case, with the module "tutorial", this would be "torii_ModuleTutorial". This class must extend the Torii module base class torii_Module.
These are all prerequisites your module should fulfill, everything else you would want to do in the module directory is up to you. There are some more points which iare established as standards, but you are free to break with them if it fits better your application.
The base class
The base class for you module need to implement three methods, which are not implemented here, but will be implemented soon.
<?php
/**
* Module for a simple todo list
*
* @version $id$
* @author Kore Nordmann <kore@php.net>
* @license GPL
*/
class torii_ModuleTutorial extends torii_Module
{
/**
* Return the modules XHTML and JavaScript
*
* @return string
*/
public function render()
{
// @TODO: Implement
}
/**
* Perform module dependant checks.
*
* Returns an array with run checks. The module should check everything
* which could fail in the later execution of the module. The resulting
* array looks like:
* array(
* 'Test 1' => true, // Test passed
* ...
* 'Sub section' => array(
* 'Test 2' => 'User readable error message.',
* ...
* ),
* ...
* )
*
* @access public
* @return array
*/
public function check()
{
// @TODO: Implement
}
/**
* Return the modules as structure (object / array), which can be
* converted to JSOn and send to the JS-Client
*
* @return torii_JSONRPC
*/
protected function getData()
{
// @TODO: Implement
}
}The initialisation
Once your module has been added to some users configuration, it will be instantiated on each request to the portal backend. On instantiation the module will be constructed from its id, configuration and its name.
/**
* Create the module from configuration
*
* @param mixed $id
* @param SimpleXMLElement $configuration
* @param string $name
* @return torii_Module
*/
public function __construct( $id, SimpleXMLElement $configuration, $name = '' )
{
$this->id = $id;
$this->name = $name;
$this->configuration = $configuration;
}All these values are usable in your module and the id can be considered as globally unique string. The configuration SimpleXMLElement contains your modules configuration, if there is some.
Render method
Let's start first with the render method - Torii will call the render method of all modules on the initial request. The returned XHTML and JavaScript will be integrated in the initial output. You should not fetch data or perform some other time consuming operations here, but just return some simple stuff. This is because the initial loading of the portal should stay fast, and every data may be fetched asynchronously later.
Usually the render method is implemented like the following example shows.
/**
* Return the modules XHTML and JavaScript
*
* @return string
*/
public function render()
{
return str_replace(
array(
'%id',
'%name',
),
array(
$this->id,
$this->name,
),
file_get_contents( dirname( __FILE__ ) . '/template.tpl' )
);
}We use a static template, which contains the XHTML and JavaScript in which re replace some placeholders, to set - for example - the correct title, or use the ID to ensure some JavaScript variables are unique.
A basic template
The template file used in the render method described above may basically look like the following and will be extended later, actually handle out module logic.
<script language="javascript" type="text/ecmascript">
torii_Manager.addModule( '%id', 3600 );
function torii_module_%id()
{
// @TODO: Implement
}
var module_%id = new torii_module_%id();
</script>
<h3>%name</h3>
<ul class="feed" id="%id_list">
<li>Loading ...</li>
</ul>This is very simple, because the module does not really do anything for now. You can see, that the placeholder "%id" is used in several places where we require a unique string, like when adding the module to the Torii module manager, or to later uniquely identify the list from JavaScript code again.
In the third line we add the module with its to the module manager, together with a number as a second parameter. This parameter specifies the interval the manager should attempt to refresh the module data in seconds. Should should not select a too short value here, because this could cause high load on the server, but also not a too big value, because this would result in too old data. For a todo list, an interval of one hour seems sensible. You might want to make this interval configurable, like the feed modules does.
Using the module
Now we can already integrate the module in our configuration and see what it does. How you do that exactly is described in the installation guide. As our module for now does not have any special configuration such a line, included in one of the sections, is enough:
<module type="tutorial" id="my_module" name="Tutorial TODO list" />The type attribute obviously references the directory name and identifier of the module. The ID, as said before, should be unique in your configuration, and the name is free to chose.
Done everything as described above you should already be able to see your module in the browser when calling the portal, like this:
Module first loaded.Beside this you will get an error in your JavaScript debugging console, which looks like this for me with Firefox.
Error: [Exception... "'Could not parse JSON response from module tutorial. Check the URL 'tutorial/renderData/' for possible errors. Module deactivated until reload.' when calling method: [nsIOnReadyStateChangeHandler::handleEvent]" nsresult: "0x8057001e (NS_ERROR_XPC_JS_THREW_STRING)" location: "<unknown>" data: no]
As the exception text says, the Torii module manager will deactivate the module, because it did not receive a valid response. This is because we did not implement the getData() method in the module class to return something the client side JavaScript manager could use.
Send data to the module
We now managed to integrate a very basic module in the portal, so now it is time to send some data to the module and display it on the client side. For thiss we need to implement the getData() method in the module class to return a valid JSONRPC object as a response to the request. The getData() of a module method will always be called, when the module manager requests data for one module.
The JSONRPC created on the server side specifies which client side method should be called with the provided data. The manager on the client side will then dispatch to correct module, but let's just implement this.
Server side
Like said we first need to implement the getData() method.
/**
* Return the modules as structure (object / array), which can be
* converted to JSOn and send to the JS-Client
*
* @return torii_JSONRPC
*/
protected function getData()
{
$todo = array();
$todo[0] = new StdClass();
$todo[0]->id = 1;
$todo[0]->entry = 'This is just a test entry for the tutorial.';
$todo[1] = new StdClass();
$todo[1]->id = 2;
$todo[1]->entry = 'Another test entry for the tutorial.';
return new torii_JSONRPC(
$this->id,
'updateTodo',
$todo
);
}In this method we just fake some todo entries in a way, like we will later could receive them from a database. With the build up data array, a torii_JSONRPC is created, which receives as a first parameter the ID of the module, the method which should be called by the manager on the client side, and the actual data.
Because we are dealing with JSON here, we use objects for the actual data, because the are mapped to JavaScript objects, which are indexed by strings instead of just numbers, which will make it easier for us to later access the data again.
To test, if the module returns the correct data you may call the URL directly the manager will use to request the data. Depending on your installation this should look like: http://portal/tutorial/renderData and you should get some JSON output without any PHP notices or warnings or errors.
{ "class":"tutorial",
"method":"updateTodo",
"content": [
{ "id":1,
"entry":"This is just a test entry for the tutorial."
},
{ "id":2,
"entry":"Another test entry for the tutorial."
}
]
}(Formatted for better readability.)
Client side
As said, on the client side the specified method "updateTodo" will be called on the module, so we need to implement this one, to update the view with the received data. For this we open the template file again and extend the JavaScript code in there.
function torii_module_%id()
{
/**
* Update displayed todo list with contents from server
*
* @param mixed $items
* @return void
*/
this.updateTodo = function( data )
{
var items = data;
var list = document.getElementById( '%id_list' );
// Remove childs from todo list
while ( list.hasChildNodes() )
{
list.removeChild( list.firstChild );
}
// Add new list items
for( var i = 0; i < items.length; i++ )
{
li = document.createElement( 'li' );
text = document.createTextNode( items[i].entry );
li.appendChild( text );
list.appendChild( li );
}
}
}The method will be called by the manager and receives in the parameter data the data we returned on the server side, which is, in our case, an array with objects containing the TODO list items.
To update the view we first remove all list items from the <ul> list, which mainly is the text "Loading ...", we added there in the template, on the first call. After this the new list items are created using the standard JavaScript DOM functions and added to the list.
This method will now be called every hour again, and if the data would change on the server side, the list would be updated again.
The result
With this data and the new client side function we now get something you could start calling a TODO list, but with no interaction until now.
Fecthed module dataIf it does not show up as expected use the prior described method to directly check the module return result and take a look at your JavaScript error console.
Add some interaction
This step will make the module a bit more complex, because we want to add a database to store the TODO list entries and enable the user to add new entries and resolve entries.
For those further details take a look at the module "todo", which just extends the stuff described in the tutorial by this. You should be able to understand the documented code. just two further notes, which might be important.
The check method
When adding dependencies on some PHP extensions or anything else not commonly installed or required by the Torii core, you should add checks for those to the yet unimplemented check() method. For the todo list with a dependency on PDO/sqlite this looks like:
public function check()
{
// Perform global checks
return array(
'PDO/Sqlite extension available' =>
( extension_loaded( 'pdo_sqlite' ) ? true :
'ext/pdo_sqlite needs to be installed.' ),
);
}The array of course may contain more the one check and you should provide a user readable message if something is missing. The result of the check methods of installed modules maybe checked by the user calling the URL http://portal/check.php.
Method dispatching
As you may notice when digging through other modules code, you will see that they also call other methods on module backend. All calls going to http://portal/modulename/somemethod/data will be dispatched by the core to the public method "somemethod" in the backend class with data string as a parameter.
This enables you to provide various different actions for your module beside just fetching data. In the todo module this is for example used to mark todo entries resolved, when calling http://portal/todo/resolveEntry/1, where 1 is the ID of the todo list entry, by the following method.
/**
* Mark todo entry as resolved
*
* @param string $url
* @return void
*/
public function resolveEntry( $entryId )
{
$db = $this->connect();
$updateStatement = $db->prepare( '
UPDATE
todo
SET
resolved = :date
WHERE
id = :entry'
);
$updateStatement->bindParam( ':date', time() );
$updateStatement->bindParam( ':entry', $entryId );
$updateStatement->execute();
}