CreatingServices

A Step by Step Guide to Creating Liberty Services

Created by: xing, Last modification: 28 Aug 2006 (17:22 UTC) by Will
This page contains two tutorials on how to create Liberty Services. The first explains how to create a Service from scratch, while the second explains how to add Services to an existing bitweaver Package. If you are not familiar with what Services are or what they are good for, please first read: LibertyServices. If you are not familiar with bitweaver Packages, you might want to read up on what those are also, though this tutorial also gives some basic instruction about creating a Package for the first time.

Creating A Liberty Service Package

Overview

Creating a service is very similar to creating a regular bitweaverPackage. This tutorial will take you though setting up the minimal files you need to create a simple service and register it with the Liberty CMS Engine.

This tutorial mostly uses the GeoPackage for its examples. The Geo Package is very simple but clearly shows the elegance and power of Liberty Services. Via Liberty Services the Geo Package makes it easy to attach location data to any type of content in bitweaver without having to modify any of the packages! So, wiki pages, blog posts, articles, users, and any new content types that might come along, like events for example, can have latitude and longitude coordiates associated with it. Via Liberty Services any Package then can automagically store this geo-spacial data, look it up, or remove it.

Files You Need

Like setting up any bitweaver package, the first thing you need is a directory in you bitweaver folder which is the name of your package. The convention is to give this directory a lowercase name:



Inside your package directory you will need a few more directories and a couple of files. The directories you will need are admin and templates.



In your package directory also add the file bit_setup_inc.php and your package class file following this naming convention: LibertyYourpackage.php. All bitweaver packages use a class to register themselves with the CMS Engine, the package class file is then, obviously where this code will go. If your package is only a service the bitweaver convention is to prefix your class file will 'Liberty'; if your package is a regular package creating a new content type like wiki or aritcles, then the convention is to prefix the file with 'Bit', e.g. BitYourpackage.php. The Geo Package is a service only package, so it's class file name is LibertyGeo.php.



There are a few more files to create that you will need. In admin create schema_inc.php. In templates you will create any templates you wish to provide as a service. Lets not create any for now. This tutorial will explain how service templates are used by Liberty a little later on, and we will create one then.



Creating Your Service Database Table

If you are creating a service, there probably will be some sort of data you want to store. Setting up your data table is the same for services as it is for packages. Since there is not that much specific about this for services, we will go over creating a data table quickly first to get it over with and then get into creating the service.

In all bitweaver Packages we define our data tables in the schema_inc.php inside the package admin folder. This is what the Geo Package schema_inc.php file looks like:


<?php
$tables 
= array(
  
'geo' => "
    content_id I4 NOTNULL,
    lat F,
    lng F,
    amsl F,
    amsl_unit C(2)
    CONSTRAINT ', CONSTRAINT `geo_ref` FOREIGN KEY (`content_id`) REFERENCES `"
.BIT_DB_PREFIX."liberty_content`( `content_id` )'
  "
);

global 
$gBitInstaller;

foreach( 
array_keys$tables ) AS $tableName ) {
    
$gBitInstaller->registerSchemaTableGEO_PKG_NAME$tableName$tables[$tableName] );
}

$gBitInstaller->registerPackageInfoGEO_PKG_NAME, array(
    
'description' => "A simple Liberty Service that any package can use to store geographic data (latitude, longitude, and above mean sea level) for any content.",
    
'license' => '<a href="http://www.gnu.org/licenses/licenses.html#LGPL">LGPL</a>',
) );
?>


In the $tables array we define each data table we want. Because Geo Package adds location data to all Liberty Content the table also includes a content_id value; this value is constrained to the same in the liberty_ccontent table. The rest of this file tells the installer to use this array to generate tables in our database and it also tells the installer some information about the package. The description and license information at the end is what is displayed in the installer and admin panel package listings so that others know what your package is all about.

Now that we have a place to store our data, we can get on to the good stuff...

Registering With Liberty Services

Get a Liberty Service GUID

To register our service we first need to define a Liberty Service GUID in Liberty for our particular service. You can check the master list of taken Liberty Service GUID's on the LibertyServices page, or look in the file LibertySystem.php in the liberty package directory. You can either create a new GUID or associate your package with an existing one. But be aware that existing Service GUID are used by other packages, and only ONE package can register with a service. You should only use an existing GUID if you are creating an alternate service to an existing one.

For Geo Package a new GUID was added to LibertySystem.php:


<?php
define
'LIBERTY_SERVICE_GEO''global_positioning' );
?>


Now that we have a GUID to register to, we can finally write some code in the package.

Register Your Service Functions

In bit_setup_inc.php we will register our service to our Liberty Service GUID. We also need to add a few other things to this file. Lets look at bit_setup_inc.php from the Geo Package and walk through the parts we need to include:


<?php
$registerHash 
= array(
    
'package_name' => 'geo',
    
'package_path' => dirname__FILE__ ).'/',
    
'service' => LIBERTY_SERVICE_GEO,
);
$gBitSystem->registerPackage$registerHash );

if( 
$gBitSystem->isPackageActive'geo' ) ) {
    require_once( 
GEO_PKG_PATH.'LibertyGeo.php' );

    
$gLibertySystem->registerServiceLIBERTY_SERVICE_GEOGEO_PKG_NAME, array(
        
'content_load_sql_function' => 'geo_content_load_sql',
        
'content_list_sql_function' => 'geo_content_list_sql',
        
'content_store_function'  => 'geo_content_store',
        
'content_expunge_function'  => 'geo_content_expunge',
    ) );
}
?>


Every package has a bit_setup_inc.php file, and it is used by bitweaver to get key information about installed packages every time a page on a bitweaver site is requested. So in the first part of the file we tell bitweaver what package this is and since we are creating a service we also tell it what Liberty Service GUID it is associated with:


<?php
//set some basic package information
$registerHash = array(
    
'package_name' => 'geo',
    
'package_path' => dirname__FILE__ ).'/',
    
'service' => LIBERTY_SERVICE_GEO,
);
//tell the bitweaver kernel about it
$gBitSystem->registerPackage$registerHash );
?>


Then we declare which functions in our package to associate with which Liberty Service methods we want to connect to:


<?php
if( $gBitSystem->isPackageActive'geo' ) ) {
    
//we include our package class so bitweaver can access it
    
require_once( GEO_PKG_PATH.'LibertyGeo.php' );

    
//here we register our service functions
    
$gLibertySystem->registerServiceLIBERTY_SERVICE_GEOGEO_PKG_NAME, array(
        
'content_load_sql_function' => 'geo_content_load_sql',
        
'content_list_sql_function' => 'geo_content_list_sql',
        
'content_store_function'  => 'geo_content_store',
        
'content_expunge_function'  => 'geo_content_expunge',
    ) );
}
?>


There are several service options available to use. There are methods for loading data, for storing data, for deleting data, for displaying templates on different page types, etc. A list of available services you can connect to is also on the LibertyServices page.

Here the geo package hooks into four of these Liberty Services:
  • content_load_sql_function
    When ever any content is loaded, and that could be wiki content, articles content, whatever, Liberty will automatigally also include whatever we specify in in our function: geo_content_load_sql
  • content_list_sql_function
    Likewise, when ever any list of content is loaded, Liberty will automatically also include whatever we specify in in our function: geo_content_list_sql
  • content_store_function
    When ever any content is stored, geo_content_store will also be called.
  • content_expunge_function
    When ever content is stored, geo_content_expunge will also be called.

The associated functions, such as geo_content_list_sql, we will define in our package class file. We will do that in just a minute.

Geo Package does not register any templates with Liberty. But it could. Here are some template registration possibilities taken from the StarsPackage, which is used for rating content, and how those work:


<?php
        
'content_body_tpl' => 'bitpackage:stars/stars_inline_service.tpl',
        
'content_list_tpl' => 'bitpackage:stars/stars_list_service.tpl',
?>


  • content_body_tpl
    inserts the associated tpl above the requested content, for example above the text of a wiki page or an article.
  • content_list_tpl
    inserts the associated tpl above every content item in a list, such as a list of wiki pages.

The associated templates would be saved in the templates directory we created earlier.

Creating Your Service Class

Create The Class In Our Class File

All package classes are built on the bitweaver kernel class. To do so we include a base class file. There are a few to choose from. For more complex packages it is good to use LibertyAttachable.php as a foundation, for a simple service like Geo Package all we need is BitBase.php. For which ever base file we want to use, we include it using the appropriate package path constant. In this case BitBase comes from the Kernel Package:


<?php
require_once( KERNEL_PKG_PATH.'BitBase.php' );
?>


Now that we have a foundation we can declare our package class. The class file and class name are the same. So in Geo Package the class name is LibertyGeo. Here is how we set it up:


<?php
class LibertyGeo extends LibertyBase {
    var 
$mContentId;

    function 
LibertyGeo$pContentId=NULL ) {
        
LibertyBase::LibertyBase();
        
$this->mContentId $pContentId;
    }
}
?>


Add Class Methods

There are several standard methods, found in almost all packages, you will probably want to add to your class. These methods are:
  • load()
  • store()
  • verify()
  • isValid()
  • expunge()

These do fundamental stuff like get data, store changes, or delete data. The details of how they work are beyond the scope of this tutorial. These are not our service functions, but some of our service functions are dependant on them, so here is what they look like in the Geo Package for easy reference and easy copying:


<?php
class LibertyGeo extends LibertyBase {
    var 
$mContentId;

    function 
LibertyGeo$pContentId=NULL ) {
        
LibertyBase::LibertyBase();
        
$this->mContentId $pContentId;
    }

    
/**
    * Load the data from the database
    * @param pParamHash be sure to pass by reference in case we need to make modifcations to the hash
    **/
    
function load() {
        if( 
$this->isValid() ) {
            
$query "SELECT * FROM `".BIT_DB_PREFIX."geo` WHERE `content_id`=?";
            
$this->mInfo $this->mDb->getRow$query, array( $this->mContentId ) );
        }
        return( 
count$this->mInfo ) );
    }

    
/**
    * @param array pParams hash of values that will be used to store the page
    * @return bool TRUE on success, FALSE if store could not occur. If FALSE, $this->mErrors will have reason why
    * @access public
    **/
    
function store( &$pParamHash ) {
        if( 
$this->verify$pParamHash ) ) {
            
$table BIT_DB_PREFIX."geo";
            
$this->mDb->StartTrans();
            if( !empty( 
$this->mInfo ) ) {
                
$result $this->mDb->associateUpdate$table$pParamHash['geo_store'], array( "content_id" => $this->mContentId ) );
            } else {
                
$result $this->mDb->associateInsert$table$pParamHash['geo_store'] );
            }
            
$this->mDb->CompleteTrans();
            
$this->load();
        }
        return( 
count$this->mErrors )== );
    }

    
/**
    * Make sure the data is safe to store
    * @param array pParams reference to hash of values that will be used to store the page, they will be modified where necessary
    * @return bool TRUE on success, FALSE if verify failed. If FALSE, $this->mErrors will have reason why
    * @access private
    **/
    
function verify( &$pParamHash ) {
        
$pParamHash['geo_store'] = array();
        if( 
$this->isValid() ) {
            
$this->load();
            
$pParamHash['geo_store']['content_id'] = $this->mContentId;
            if(!empty( 
$pParamHash['geo'])){
             if( isset( 
$pParamHash['geo']['lat'] ) && is_numeric$pParamHash['geo']['lat'] ) ) {
                  
$pParamHash['geo_store']['lat'] = $pParamHash['geo']['lat'];
             }
             if( isset( 
$pParamHash['geo']['lng'] ) && is_numeric$pParamHash['geo']['lng'] ) ) {
                  
$pParamHash['geo_store']['lng'] = $pParamHash['geo']['lng'];
             }
             if( isset( 
$pParamHash['geo']['amsl'] ) && is_numeric$pParamHash['geo']['amsl'] ) ) {
                
$pParamHash['geo_store']['amsl'] = $pParamHash['geo']['amsl'];
             }
             if( !empty( 
$pParamHash['geo']['amsl_unit'] ) ) {
                  
$pParamHash['geo_store']['amsl_unit'] = $pParamHash['geo']['amsl_unit'];
             }
            }
        }
        return( 
count$this->mErrors )== );
    }

    
/**
    * check if the mContentId is set and valid
    */
    
function isValid() {
        return( @
BitBase::verifyId$this->mContentId ) );
    }

    
/**
    * This function removes a geo entry
    **/
    
function expunge() {
        
$ret FALSE;
        if( 
$this->isValid() ) {
            
$query "DELETE FROM `".BIT_DB_PREFIX."geo` WHERE `content_id` = ?";
            
$result $this->mDb->query$query, array( $this->mContentId ) );
        }
        return 
$ret;
    }
}
?>


Add Service Functions

Our service functions do not go in our class, they stand alone. For each function we registered with Liberty Services, we define it here.

Recall that in our Geo example we registered four functions:
  • geo_content_load_sql
  • geo_content_list_sql
  • geo_content_store
  • geo_content_expunge

The first two, the sql functions, actually inject sql into the load() methods of other tables. We explicitly specify each data column we want returned and typically use a LEFT JOIN to attach the service sql. load_sql and list_sql are in many cases identical or nearly identical.

Here is what they look like iin the Geo Package:

<?php
function geo_content_load_sql() {
    global 
$gBitSystem;
    
$ret = array();
    
$ret['select_sql'] = " , geo.`lat`, geo.`lng`, geo.`amsl`, geo.`amsl_unit`";
    
$ret['join_sql'] = " LEFT JOIN `".BIT_DB_PREFIX."geo` geo ON ( lc.`content_id`=geo.`content_id` )";
    return 
$ret;
}

function 
geo_content_list_sql() {
    global 
$gBitSystem;
    
$ret = array();
    
$ret['select_sql'] = " , geo.`lat`, geo.`lng`, geo.`amsl`, geo.`amsl_unit`";
    
$ret['join_sql'] = " LEFT JOIN `".BIT_DB_PREFIX."geo` geo ON ( lc.`content_id`=geo.`content_id` )";
    return 
$ret;
}
?>


Our store and expunge (fancy word for delete) are slighly different, they actually receive a hash from the package using the service, and invoke the service class methods. This is very clear in the expunge example here. The store example is a little less clear, we check to see if it doesn't execute properly, and if there are errors we capture them. Look for this line in the geo_content_store function:


<?php
if ( !$geo->store$pParamHash ) ) {
?>


Thats the Geo class method store() being called. Here are the complete Geo store and expunge service functions:


<?php
function geo_content_store( &$pObject, &$pParamHash ) {
    global 
$gBitSystem;
    
$errors NULL;
    
// If a content access system is active, let's call it
    
if( $gBitSystem->isPackageActive'geo' ) ) {
        
$geo = new LibertyGeo$pObject->mContentId );
        if ( !
$geo->store$pParamHash ) ) {
            
$errors=array('geo'=> $geo->mErrors['geo']);
        }
    }
    return( 
$errors );
}

function 
geo_content_expunge( &$pObject ) {
    
$geo = new LibertyGeo$pObject->mContentId );
    
$geo->expunge();
}
?>


And we are done!

With these four functions in place, you can install your package and Liberty Services will automagically call these functions at the appropriate times. This is all you have to do to create a Liberty Service. It is quite easy! Remember, we didn't register or create any templates in this example, but if you want to you can. Just remember to add template files you register to your template directory. You will use smarty just the same as you would for any other template. For examples see the templates in the StarsPackage or PigeonholesPackage.


Adding Liberty Services to an Existing Package

Register The Package As Service

to turn any package into a service, you need to modify your <pkg>/bit_setup_inc.php file and declare your package into a service. this can be done by modifying the way the package is registered:

<?php
$gBitSystem
->registerPackage'pigeonholes'dirname__FILE__).'/'TRUELIBERTY_SERVICE_CATEGORIZATION );
?>

explaining the parameters of registerPackage:
  1. name of the package
  2. package path
  3. specify if the package can be activated
  4. specify what type of service the package belongs to

the service type needs to be defined as a constant in liberty/LibertySystem.php - this is also the place where you can check what types of services we already have and if your's fits into one of the existing ones, please use one of those.

Register The Services

This is the cruicial bit. we register the service and when to display what and where.


<?php
if( $gBitSystem->isPackageActive'pigeonholes' ) ) {
    
$gLibertySystem->registerServiceLIBERTY_SERVICE_CATEGORIZATIONPIGEONHOLES_PKG_NAME, array(
        
'content_display_function' => 'pigeonholes_content_display',
        
'content_preview_function' => 'pigeonholes_content_preview',
        
'content_edit_function' => 'pigeonholes_content_input',
        
'content_store_function' => 'pigeonholes_content_store',
        
'content_expunge_function' => 'pigeonholes_content_expunge',
        
'content_edit_tab_tpl' => 'bitpackage:pigeonholes/pigeonholes_input_inc.tpl',
        
'content_view_tpl' => 'bitpackage:pigeonholes/display_members.tpl',
        
'content_nav_tpl' => 'bitpackage:pigeonholes/display_paths.tpl',
    ) );
?>


You can see that the pigeonholes package first checks to see if it's active, if so, it adds the service specifications.

Create Your Service Functions and tpls

For each function and tpl you register in bit_setup_inc.php you need create. Templates you will create and save in your template folder. Your service functions get defined in your class file, in Pigeonholes this is the Pigeonholes.php. But note that these functions are not IN the class, they reside outside. There are a variety of functions you can register, some add sql and some call on your package methods. Here is one example which extends all content getList methods:


<?php
function pigeonholes_content_list_sql ( &$pObject$pParamHash '') {
    global 
$gBitSystem;
    
$ret = array();
    if( 
/*$gBitSystem->isFeatureActive( 'pigeonholes_categorize_'.$pObject->getContentType() ) && */ !empty( $pParamHash['pigeonholes'] ) ) {
        if ( 
is_array$pParamHash['pigeonholes'] ) ) {
            
$ret['join_sql'] = "LEFT JOIN `".BIT_DB_PREFIX."pigeonhole_members` pm ON (lc .`content_id`= pm.`content_id`)";
            
$ret['where_sql'] = ' AND pm.`parent_id` in ('.implode','array_fill(0count$pParamHash['pigeonholes']  ), '?' ) ).')';
            
$ret['bind_vars'] = $pParamHash['pigeonholes'];
        } else {
            
$ret['join_sql'] = "LEFT JOIN `".BIT_DB_PREFIX."pigeonhole_members` pm ON (lc .`content_id`= pm.`content_id`)";
            
$ret['where_sql'] = " AND pm.`parent_id`=? ";
            
$ret['bind_vars'][] = $pParamHash['pigeonholes'];
        }
    }
    return 
$ret;
}
?>


Once you have defined all your functions you are done. Liberty Services automagically does the rest.
</pkg>