Module Architecture for Claroline

From ClaroDevel

Table of contents

Warning

The Following is only a proposal

This Module architecture is not directly related to the plug-ins(*) architecture but is more about refactoring the current Claroline libraries API.

(*) Plug-ins could implement a similar architecture and/or use this architecture to access the Claroline API.

The Goal

An architecture to enhance library loading.

I am currently working on a new architecture for the Claroline libraries and include files. The goal of this architecture is to make Claroline libraries organisation clearer and to make library loding more easy.

The Idea

overview of module architecture
Enlarge
overview of module architecture

The main idea is :

  • Organise all related libraries and class definition files into a same directory (for example user, course, forum, kernel...) called modules. The directory for a module named MODULE_NAME is MODULE_NAME
  • Provide and entry point for each module in the form of an include file which contains le inclusion of every file needed by a module and named after inc.MODULE_NAME.php where MODULE_NAME is the name of the module (ie the name of the directory containing this modules files).
  • Create a way to identify the modules : for example 'Kernel::Database'
  • Provide a function to load those modules : for example load_module( 'Kernel::Database' ); This function also include code to improve debug by storing all loaded modules into the $GLOBALS array;

The last step is not mandatory. Another solution is to load the module using

require_once MODULE_PATH/inc.MODULE_NAME.php;

Some advantages of this architecture

  • the only mandatory files to load manualy in scripts are claro_init* script and the file that contains the load_module function definition. In my implementation, load_module is located into the core module so I have to manualy include it using
require_once $moduleIncludePath . '/core/inc.core.php'; 
  • the developers are not required to know where the modules are located since local_init could load le lib containing the load_module function
  • the developer does not have to worry about the satisfaction of library dependencies since it is handle by the module entry points
  • the generation of module entry point could be automated
  • the library is more structured
  • the details of module loading is hidden and so could be easily modified without the developer notice it
  • only module names matter, not their location
  • automation : some repetitive tasks could be automated :
    • module entry point generation with cross module requirement handling (when one module rely on another to work)
    • test case generation for rapid test skeletton generation based on module name

This architecture already works on my computer for some development libraries and classes.

Some issues or difficulties

  • you have to access global variable into module by using either global or $GLOBALS, or through an API because since module files are required from a function they cannot access variables that are not local to this function. In PHP 5, this can be handle using the __autoload function. This coulmd also be handle using the DotClear approach : inter-module communication accurs using functions that encapsulate the call to $GLOBALS array (see Dotclear l10n architecture or plugin buffer for example)
  • you have to follow some rules to create your modules
  • module developers have to cleary document their API
  • module users have to know the name of the modules so they have to be documented

Code

The main function

Here is the code for the load_module function (without the DEBUG code)

  /**
    * load module from global moduleIncludePath
    */
   function load_module( $name )
   {
       global $moduleIncludePath;        
       // create module path
       $path = module_path_from_fullname( $name );
       $module = module_name_from_fullname( $name );
       // use realpath to ensure require_once
       $moduleFile = realpath( $moduleIncludePath
           . "/" . $path
           . "/inc." . $module . ".php"
           );    
       // include module file
       if ( file_exists( $moduleFile ) )
       {
           require_once $moduleFile;
           return true;
       }
       else
       {
           return false;
       }
   }


Note that one can replace the line

global $moduleIncludePath;

with

$moduleIncludePath = dirname(__FILE__) . RELATIVE_PATH_TO_MODULE_DIRECTORY;

that is more secure.

For example, in my implementation, the load_module is located into the 'Core' module so I could write

$moduleIncludePath = dirname(__FILE__) . '/..';

Module resolver functions

Get path from full name

Full name is of the form Module(::SubModule)*. For example 'Core::Database' become 'core/database'

   function module_path_from_fullname( $name )
   {
       $ret = strtolower( $name );
       $ret = str_replace( '::', '/', $ret );
       return $ret;
   }

Note that this module architecture uses UNIX path separator since it works on any PHP plateform.

Get name from full name

Returns the last part of the module full name. For example 'Core::Database' gives 'database'

   function module_name_from_fullname( $name )
   {
       $ret = strtolower( $name );
       $ret = explode( '::', $ret );
       return $ret[(count($ret) - 1)];
   }

A sample module entry point

Here is a sample entry point for the module named MODULE_NAME.

This file has been generated automaticaly by my 'makemodule' script but I added some comment to make it easier to understand.

if( ! defined( "MODULE_NAME_MODULE_ALREADY_LOADED" ) )
{
define( "MODULE_NAME_MODULE_ALREADY_LOADED", __FILE__ . ' : ' . __LINE__ );	
// use load_module function to load required modules
if ( function_exists('load_module') )
{
load_module('Core::Database');
load_module('Pattern::MVC');
}
// use PHP require infrastructure if load_module not available
else
{
require_once $moduleIncludePath . "/core/database";
require_once $moduleIncludePath . "/pattern/mvc";
}	
// module files hereafter
require_once dirname(__FILE__) . "/class.module.php";
require_once dirname(__FILE__) . "/lib.module.php";
}

At this time the script does not handle the order of the module files so they have to resolve their cross-references themselves by using require_once.

To use the Module dummy module just call

load_module( 'Module' );

from your script. This will load the Module lib.module.php and class.module.php files and all dependencies ie:

  • Core::Database module
  • Pattern::MVC module

Architecture usage example

Here is a dummy example

// load modules
load_module( 'Core::Log::File' );
load_module( 'Display::MessageBox' );
// application code here
$log = new LogFile( 'log.txt' ); // from Core::Log::File
$log->append( 'foo' );
$bar = $log->read();
MessageBox::Question( $bar ); // from Display::MessageBox

Tools

The following is NOT complete !!!!

Module entry point generator

Module entry point generator command line options :

$ php sdk/makemodule/makemodule.php -h
Usage: sdk/makemodule/makemodule.php -opt=val -switch ...
Known options and switches are detailed below.
[R] means the option is required,
[S] stands for valueless switch, otherwise
    option requires an value,
[M] means you can use this option as many times,
    as you need,
 -s -src       [R] Module Source directory
 -w -with      Comma separated list of required modules
 -m            [S] Use Module library if available
 -r -recursive Recurse into module source directory subdirectories
 -e -ext       Comma separated list of php source file extensions
 -x -exclude   Comma separated list of files or directories to exclude fr
               om module relative to module source directory
 -c -conf      Configuration file path
 -i -inc       Required modules include path
 -h -help      [S] Display this page
 -v -version   [S] Display version
 -d -debug     [S] Debug mode


Test Case generator for a module

Module test case generator command line options :

$ php sdk/maketest/maketest.php -h    
Usage: sdk/maketest/maketest.php -opt=val -switch ...
Known options and switches are detailed below.
[R] means the option is required,
[S] stands for valueless switch, otherwise
    option requires an value,
[M] means you can use this option as many times,
    as you need,
 -m -module   [R] Module name (for example Core::Buffer)
 -r -root     Test root directory
    -nobackup [S] Do not backup existing test file
 -h -help     [S] Display this page
 -v -version  [S] Display version
 -d -debug    [S] Debug mode