avatar vipse Jan 2. 2009. 11:59 am
**** I am very glad this thread has been tagged as "Important". Thank you aC Staff! ****

----last update: Feb 18, 2009 --------

I was trying to develop a module (plugin) for ac in the past couple days. Because there is not a really detailed step-by-step document for module development. It really took me a while to finally get what I want.

Now I am writing down my experience as a rough guide for developing an ac module. Hope it could help any of you and feel free to correct me on my mistakes. (To admin: please go ahead directly edit my post or add notes for any serious misleading issues.)

Basically how I learned to develop modules is to look at codes from official modules such as checklists and timetracking.

1. Location of the modules
All modules are placed in activecollab/application/modules. You can put your own modules under this directory and you will see them from the ac administration panel, under "Modules" page.

2. File structure of a module
From any other modules, you will see the following structure. What I did when I developing my module is just following the structure.
controllers ------- Controller action classes are here.
handlers ------- Handler classes are here. They define how this module behaves upon different events in ac. E.g. if you want your module shows on the tab in project detail page, you will need a on_project_tabs.php; if you want your module shows a link in the quick add list, you will need a on_quick_add.php.
helpers -------- Helper functions for the view layer (which are custom helper functions for the Smarty template engine ac using in its view layer).
layouts -------- If your module is going to have its whole new style, you will need write smarty template files for the overall design (header, footer stuff) of the new style.
modules -------- Model classes are here.
views -------- Smarty templates for the view layer.

handlers.php-------- Registers your own defined handler functions to ac events.
info.php -------- Provides module description, version info and uninstallation warning message.
init.php -------- Defines module name, path information, global functions.
lang_index.php and lang_index_js.php --------- I *think* they are for the {lang} tag in the smarty template files.
routes.php -------- This file basically encodes all the URL link patterns, use assemble_url() inside your controller to generate actual urls.

3. Model (Persistence) Layer
Now we can start to think about the design of model layer. Usually, a new module will introduce one or more object classes to deal with. There are two choices of writing an object class:
(1) if you want to have a separate database table to store the your object, you should write your class as a subclass of ApplicationObject. You must specify all the fields as they are in the db table.
class Note extends ApplicationObject {

    /**
     * All table fields
     *
     * @var array
     */
    var $fields = array('id', 'body');
    
    /**
     * Primary key fields
     *
     * @var array
     */
    var $primary_key = array('id');
    
    /**
     * Name of AI field (if any)
     *
     * @var string
     */
    var $auto_increment = 'id'; 
    
    /**
     * Construct the object and if $id is present load record from database
     *
     * @param mixed $id
     * @return 
     */
    function __construct($id = null) {
      $this->table_name = TABLE_PREFIX . 'mediabuyings';
      parent::__construct($id);
    }

    function getId() {
      return $this->getFieldValue('id');
    }
    
    function setId($value) {
      return $this->setFieldValue('id', $value);
    }

    function getBody() {
      return $this->getFieldValue('body');
    }
    
    function setBody($value) {
      $this->setFieldValue('body', $value);
    } 
  ... ...
}

How about the db table you are having in mind? Yes, you need to write a install.php contains sql queries of creating the db table. See install.php in the tickets module for the example.

(2) An alternative way to manage your objects is reuse the project_object table, this table has many basic fields such as user, created time, updated time. It also has a few integer, float and string fields to store your customized information. So what you have to do is write your class as a subclass of ProjectObject.
Such as Checklist.class.php in the checklists module. You can declare which fields you want to use in your case. (however I think id, type and module are mandatory).
    var $fields = array(
      'id', 
      'type', 
      'module', 
      'name',
      'integer_field_1',
      'float_field_1',
       ...
     );


Now we have the model class. We should have another management class for manipulating model objects. It should contains common data retrieving functions such as findById(), findAll(), findByProject() etc., depending the design. For ApplicationObject models, management class can extends DataManager;
    function find($arguments = null) {
      return DataManager::find($arguments, TABLE_PREFIX . 'your_table_name', 'YourModelClassName');
    } // find

And for ProjectObject models, retrieving will be handled by the helper functions provided by ProjectObject:
ProjectObjects::find(array(
        'conditions' => array('project_id = ? AND type = ? AND state >= ? AND visibility >= ?', $project->getId(), $YourModule, $min_state, $min_visibility),
        'order' => 'created_on',
      ));


Naming practice of models in ac is singular for model class, plural for management class. But it's totally up to you. You will need to load them manually in the init.php anyway:
  set_for_autoload(array(
    'YourModel' => YOUR_MODULE_PATH . '/models/YourModel.class.php',
    'YourManagement' => YOUR_MODULE_PATH . '/models/YourManagement.class.php'
  ));


4. Controller Layer
Basically you will need a controller class to dispatch different action requests such as index, add, edit and delete. In each function of the class, you process data based on request parameters and use smarty->assign to store the data you want to display in the template.

First of all, a very different but useful design pattern in ac is to put common codes into the constructor. Usually in a constructor of an ac controller class, we will have codes dealing with adding breadcrumbs, retrieve an object if there is a request parameter named "id", assemble common URLs etc.
//constructor example
function __construct($request){
  //add some breadcrumb
  $this->wireframe->addBreadCrumb(lang('Your Module'), assemble_url('yourmappedurl', $this->active_project->getId()));
  //build some url
  $add_a_new_object_url = assemble_url('yourmappedurl', $this->active_project->getId());
  //deal with request id
  $object_id = $request->getId('id');
  if($object_id) {
    $this->active_your_object = YourDataManager::findById($object_id);
  }
  //assign with smarty
  $this->smarty->assign(array(
    'active_your_object'       => $this->active_your_object,
    'add_a_new_object_url'     => $add_a_new_object_url,
  ));
}


Consider the index page, we will usually want to list all objects. So a typical action will be like:
//index example
function __index($request){
  //retrieve all objects and assign with smarty
  //assign with smarty
  $this->smarty->assign(array(
    'all_objects'       => YourDataManager::find()
  ));
}


Add/Edit pages are similiar except edit page will need display the old value of the object. This is achieved by the following example code in the edit action:
      $your_object_data = $this->request->post('your_object');
      if(!is_array($your_object_data)) {
        $your_object_data = array(
          'field1' => $this->active_your_object->getField1(),
          'field2' => $this->active_your_object->getField2(),
          'field3' => $this->active_your_object->getField3(),
          'field4' => $this->active_your_object->getField4(),
        );
      } // if
      
      $this->smarty->assign('your_object_data', $your_object_data);

We can observe that this piece of codes will work in both displaying the form (the "if" body) and processing the submitted form.

5. Presentation Layer
If you want your controller action shows a web page eventually, you will need a smarty template, inside of which you will display the variables assigned by "$this->smarty->assign" in the action.
aC has a lot of built-in custom smarty tags that can really save your time, such as {title}, {add_bread_crumb}, {select_user}, {select_project} tag. They are usually named function.tag_name.php. When there's need, you can also write your tags for future reuse.

Besides aC's own tags, there's not much to say about the templates. They're standard smarty (http://www.smarty.net/manual/en/).
Some of the most useful smarty functions/tags include {assign}, {foreach}, {$your_variable|filter1|filter2}, {link}, {form}, {if}.

Yes, filters are very useful. For example you want to trim a long text to the size of 30, just use "excerpt" like {$object->getBody()|exerpt:30}.

6. Some tips when developing aC
Personally I prefer Eclipse with PHPEclipse plugin to develop aC. You can use Ctrl+Shirt+R (Open Resource) to open most of the php classes directly. For example you want to open the class for Task, just use Ctrl+Shift+R and enter Task. From the naming practice we know that "Task" is the actual model, "Tasks" contains business logics of Task and TasksController is the controller of the built-in Task module.

Also, open Ctrl+Shift+R and enter "function.*", you will get most of the aC customized smarty helper functions (other types of helpers are "block.*", "modifier.*"). Looking into these source codes will let you know what parameters these functions accept.

Although not very precise, the auto-completion feature of PHPEclipse can also help us a lot. Global variables defined by aC such as object state beginning with STATE_ and visibility beginning with VISIBILITY_, as well as global functions such as database functions beginning with db_, can also be auto-completed.
---------------to be continued-------------
avatar Ilija Studen Staff Jan 2. 2009. 12:44 pm
Thanks for taking time to write this post. There is no module development documentation so all content explaining it is more than welcome.

My main problem with this is that I don't have a perspective of someone new to module development so I don't know which subjects to cover first. There are four articles in Developer Blog that describe some of the key concepts used by modules:

1. Anatomy of activeCollab Modules
2. Controllers
3. Request
4. Persistence Layer

If you have any specific questions, or if there are things you wish you knew when you first started, please post them and I'll see to write articles covering them in more details when I find some time.

Thanks again!
avatar vipse Jan 2. 2009. 1:01 pm
Ilija Studen:
Thanks for taking time to write this post. There is no module development documentation so all content explaining it is more than welcome.

My main problem with this is that I don't have a perspective of someone new to module development so I don't know which subjects to cover first. There are four articles in Developer Blog that describe some of the key concepts used by modules:

1. Anatomy of activeCollab Modules
2. Controllers
3. Request
4. Persistence Layer

If you have any specific questions, or if there are things you wish you knew when you first started, please post them and I'll see to write articles covering them in more details when I find some time.

Thanks again!


Hi Ilija, yes I am aware of those articles but it's just difficult for me to put the knowledge from these articles together to form a module. For example the routing, handler.

I think it will be helpful if you could list all the available events "registrable" for the handlers.
avatar Ilija Studen Staff Feb 17. 2009. 4:18 pm
Question about save method problem moved into a new topic: Problem with save() method (changes don't get saved)
avatar simcoury May 18. 2009. 9:22 pm
Hi Vipse, thanks for this post.
avatar chrisbloom7 Aug 24. 2009. 1:36 pm
Is there a list of the handlers available for each module? I looked in the developers manual, bit didn't see anything there.
avatar Ilija Studen Staff Aug 24. 2009. 1:45 pm
There is currently no document that describes available events, but you can search for event_trigger() function and you'll have all places where events are called.
avatar chrisbloom7 Aug 24. 2009. 2:36 pm
Ilija Studen:
There is currently no document that describes available events, but you can search for event_trigger() function and you'll have all places where events are called.


Thanks for pointing me to that function. FWIW, here's a list of the events that are called in the system (as of 2.1.3)

on_admin_sections
on_after_init
on_after_object_save
on_after_object_validation
on_before_init
on_before_object_deleted
on_before_object_insert
on_before_object_save
on_before_object_update
on_before_object_validation
on_before_save_project
on_build_menu
on_class_missing
on_comment_added
on_comment_deleted
on_comment_updated
on_company_options
on_company_quick_options
on_company_tabs
on_copy_project_items
on_daily
on_dashboard_important_section
on_dashboard_sections
on_frequently
on_get_completable_project_object_types
on_get_day_project_object_types
on_get_project_object_types
on_hourly
on_master_categories
on_milestone_add_links
on_milestone_objects
on_milestone_objects_by_visibility
on_new_revision
on_object_deleted
on_object_inserted
on_object_updated
on_portal_milestone_add_links
on_portal_milestone_objects
on_portal_object_quick_options
on_portal_permissions
on_prepare_email
on_project_completed
on_project_created
on_project_deleted
on_project_export
on_project_object_completed
on_project_object_copied
on_project_object_locked
on_project_object_moved
on_project_object_opened
on_project_object_options
on_project_object_quick_options
on_project_object_ready
on_project_object_reassigned
on_project_object_restored
on_project_object_trashed
on_project_object_unlocked
on_project_opened
on_project_options
on_project_overview_sidebars
on_project_permissions
on_project_tabs
on_project_updated
on_project_user_added
on_project_user_removed
on_project_user_updated
on_quick_add
on_shutdown
on_system_permissions
on_time_report_footer_options
on_user_cleanup
on_user_options
on_user_quick_options


And here it is again sorted by module and with argument list
AngieObject->AngieApplication
on_after_init
on_before_init
on_shutdown

AngieObject->ApplicationMailer
on_prepare_email, array($tpl, $recipient_email, $context, $body, $subject, $attachments, $language)

AngieObject->DataObject
on_after_object_save, array('object' => $this)
on_after_object_validation, array('object' => $this, 'errors' => $errors)
on_before_object_deleted, array('object' => $this)
on_before_object_insert, array('object' => $this)
on_before_object_save, array('object' => $this)
on_before_object_update, array('object' => $this)
on_before_object_validation, array('object' => $this)
on_object_deleted, array('object' => $this)
on_object_inserted, array('object' => $this)
on_object_updated, array('object' => $this)

AngieObject->ProjectExporterOutputBuilder
on_project_export, array($exportable_modules, $this->active_project)

ApplicationController->AdminController
on_admin_sections, array($admin_sections)

ApplicationController->DashboardController
on_dashboard_important_section,array($important_items, $this->logged_user)
on_dashboard_sections, array($dashboard_sections, $this->logged_user)
on_quick_add, array($quick_add_urls)

BaseCompany->Company
on_company_options, array($this, $options, $user)
on_company_quick_options, array($this, $options, $user)

BaseProject->Project
on_before_save_project, array('project' => $this)
on_copy_project_items, array($this, $to, $milestones_map)
on_project_completed, array($this, $by, $status)
on_project_created, array($this, $template)
on_project_deleted, array($this)
on_project_opened, array($this, $status)
on_project_options, array($options, $this, $user)
on_project_updated, array($this)
on_project_user_added, array($this, $user, $role, $permissions)
on_project_user_removed, array($this, $user)
on_project_user_updated, array($this, $user, $role, $permissions)

BaseProjectObject->ProjectObject
on_portal_object_quick_options, array($options, $this, $portal, $commit, $file)
on_project_object_completed, array($this, $by, $comment)
on_project_object_copied, array($this, $copy, $project, $cascade)
on_project_object_locked, array($this, $by)
on_project_object_moved, array($this, $old_project, $project)
on_project_object_opened, array($this, $by)
on_project_object_options, array($options, $this, $user)
on_project_object_quick_options, array($options, $this, $user)
on_project_object_ready, array($this)
on_project_object_reassigned, array($this, $this->old_assignees, array($assignees, $owner_id))
on_project_object_restored, array($this)
on_project_object_trashed, array($this)
on_project_object_unlocked, array($this, $by)

BaseProjectObject->ProjectObject->Comment
on_comment_added, array($this, $parent)
on_comment_deleted, array($this, $parent)
on_comment_updated, array($this, $parent)

BaseTimeReport->TimeReport
on_time_report_footer_options, array($this, $this->footer_options, $project, $user)

BaseUser->User
on_user_cleanup, array($cleanup)
on_user_options, array($this, $options, $user)
on_user_quick_options, array($this, $options, $user)

DataManager
on_class_missing, array($item_class)

PageController->CronController
on_daily
on_frequently
on_hourly

PeopleController->CompaniesController
on_company_tabs, array($tabs, $this->logged_user, $this->active_company)

Permissions
on_portal_permissions, array($permissions)
on_project_permissions, array($permissions)
on_system_permissions, array($permissions)

ProjectsController->ProjectController
on_master_categories, array($category_definitions)
on_project_overview_sidebars, array($home_sidebars, $this->active_project, $this->logged_user)
on_project_tabs, array($tabs, $this->logged_user, $this->active_project)

ProjectsController->ProjectController->FilesController
on_new_revision, array($this->active_file, $last_revision, $this->logged_user)

ProjectsController->ProjectController->MilestonesController
on_milestone_add_links, array($this->active_milestone, $this->logged_user, $links)
on_milestone_objects_by_visibility, array($milestone, $objects, $object_visibility)

ProjectsController->ProjectController->Milestone
on_milestone_objects, array($this, $objects, $user)
on_portal_milestone_add_links, array($this, $links, $portal)
on_portal_milestone_objects, array($this, $portal_objects, $portal)

ProjectsController->ProjectController->PagesController
on_new_revision, array($this->active_page, $new_version, $this->logged_user)

ProjectsController->ProjectController->ProjectExporterController
on_project_export, array($exportable_modules, $this->active_project)

SettingsController->CategoriesAdminController
on_master_categories, array($category_definitions)

function.menu.php
on_build_menu, array($menu, $logged_user)

functions.php
on_get_completable_project_object_types, array(), array()
on_get_day_project_object_types, array(), array()
on_get_project_object_types, array(), array()
avatar Frederik (Dwarf) Pro Sep 10. 2009. 6:05 am
Would it be possible for you guys at ActiveCollab to make some sort of wiki pages where us developers could contribute on how to make modules for aC?

This very small guide is outdated as many of the module information has been moved from individual files (handlers.php, info.php etc.) to the module class.

Although I haven't gotten very far in my development yet, some of the issues I've already encountered could probably have been easened with help from user contributed wiki pages.
Best regards

Frederik Sauer
Dwarf A/S
avatar Ilija Studen Staff Sep 10. 2009. 6:44 am
We'll see about adding wiki to /docs section, thank you for the suggestion.
or Go To Next Page