Feng Office Plugin Tutorial: Hello World

In this tutorial we will create a minimal plugin that shows a simple message in a Feng Office tab. This should provide a basic understanding of how the Feng Office Plugin System works.

A tutorial for a less “trivial” example is being developed and can be found here.

First, choose a plugin name, and create a folder with that name in the Feng Office plugins folder:

FENGOFFICE_ROOT/plugins/PLUGIN_NAME

where FENGOFFICE_ROOT is the document root for Feng Office on your server and PLUGIN_NAME is the name of your new plugin, in our case 'helloworld', for example:

/public_html/fengoffice/plugins/helloworld

All the necessary resources and code for your plugin will be placed in this folder, including javascripts, css, images and, of course, the application php (templates, views, controllers, models). One exception is the theme-specific CSS, which must be inserted in the relevant CSS stylesheet within your theme.

The first file you need to create is the plugin metadata 'info.php'. Create this file under your plugin root folder 'plugins/PLUGIN_NAME/info.php' containing this php code:

info.php
<?php return array(
    "name"        => "helloworld",            // plugin name
    "version"     => "1",                     // plugin version
    "author"      => "Feng Office",           // author name
    "website"     => "http://fengoffice.com", // author website URL
    "description" => "My first Feng plugin"   // brief description	
);?>

This will provide information about the plugin version, description, order, dependencies, category, author and other metadata necessary for third party plugin management.

However, if you want to create a new tab along with your plugin, you will need to specify it with a tab definition follows:

info.php
<?php return array(
    "name"        => "helloworld",            // plugin name
    "version"     => "1",                     // plugin version
    "author"      => "Feng Office",           // author name
    "website"     => "http://fengoffice.com", // author website URL
    "description" => "My first Feng plugin",  // brief description	
    "tabs" => array(                          // tab definition
       array(
         "id"                        => "helloworld-panel", //tab id
         "ordering"                  => 2,                  //tab ordering position
         "title"                     => "helloworld tab",   //tab title (lookup key for lang file)
         "icon_cls"                  => "ico-world",        //CSS class for tab's icon
         "refresh_on_context_change" => true,               //re-run the controller if e.g. new workspace is chosen
         "default_controller"        => "helloworld",       //default controller
         "default_action"            => "say_hello" ,       //default action(method) called when tab is selected or context changes - used to list objects, etc.
         "initial_controller"        => "" ,                //initial controller
         "initial_action"            => "" ,                //initial action(method) called once per session - can be used to load js scripts, etc.
         "type"                      => "plugin",           //type - use "plugin"
         "object_type_id"            => 0                   //id of the content object if it is related to one, otherwise 0                        
                )
     )
);?>        

Note: Please look below at the 'Email' plugin example for further details regarding a content object association.

This tells the installer that it will have to create a new tab in the system, specifying id, title and many other configuration options, but the most important ones are default_controller and default_action.

The tab title will be looked up in a language file. In our case the lookup string will be “helloworld tab”. We need to provide a return string in the language file. Create the language folder 'plugins/PLUGIN_NAME/language/LANGUAGE', in our case 'plugins/helloworld/language/en_us'. Two files are placed into this folder: 'lang.js' and 'lang.php'. Simply copy the lang.js script from another plugin, and create 'lang.php' as follows:

lang.php
<?php return array(
	'helloworld tab'=>'World',
);

This will display 'World' as the our new tab's title. For multi-lingual support you may repeat this process for other languages.

At this point we assume that you are familiar with MVC concepts. If not, we strongly suggest you explore the topic before proceeding.

Create the folder 'application/controllers' under your plugin structure:

 FENGOFFICE_ROOT/plugins/PLUGIN_NAME/application/controllers

Then create the following class under this folder:

[PLUGIN_NAME]Controller.class.php

That is, in our example (note the CamelCase):

HelloworldController.class.php

…with the following content:

HelloworldController.class.php
<?php class HelloworldController extends ApplicationController {                
        var $plugin_name = "helloworld"; //name of the plugin
 
        function __construct() {
                parent::__construct();
                prepare_company_website_controller($this, 'website');              
        }
 
        function say_hello() {
                $txt = "Hello World ! ! ! ! ";
                tpl_assign('message',$txt);
        }
}?>

In this case the controller loads the data, and assigns it to the view in a variable called 'message'.

Notes:

  • We have not spoken about models yet, so the data is hard-coded in order to simplify the example.
  • This is not what the actual Mail plugin does, but it has been used this way to explain it better

Create the folders 'application/views/helloworld' under your plugin structure so that it looks like: FENG_ROOT/plugin/PLUGIN_NAME/application/views/OBJECT_NAME

i.e.: FENG_ROOT/plugin/helloworld/application/views/helloworld

Within the last folder, create the following file: 'say_hello.php' with the following content:

say_hello.php
<div class="hello-world">
        <h1><?php echo $message ?></h1>
        <p>Congratulations ! ! !</p> 
        <p>This is your first hello world application. <p>
</div>

Note: When invoking a controller action (or method talking in OOP), the system automatically loads the view with the same name; controller function 'ACTION()' will invoke view 'ACTION.php'.

In info.php we set the array element

"icon_cls" => "ico-world"

and now we need to create a CSS class .ico-world that styles a background with the desired icon image.

Create CSS stylesheet for the theme's tab at [FENG_ROOT]/plugins/helloworld/public/assets/css/helloworld.css and insert the class .ico-world. You may also create your an image folder for your plugin at [FENGOFFICE_ROOT]/plugins/helloworld/public/assets/images or, if suitable images are already available, simply use them instead, as we do here, i.e., the default theme's 16×16 sprites with a vertical offset of -357 pixels.

helloworld.css
.ico-world {
	background: transparent url('/public/assets/themes/default/images/16x16/all_16_16_vertical.png') no-repeat scroll 0 -357px !important;
}

Custom CSS and JS can be done in many ways:

INLINE (not recommended):

Each view can make use of <script> and <style> tags, but this does not follow the best practices of web development

Automatically load the css JS (recommended):

While loading a page, Feng Office will include the JS and css on its head section if you create those files following this convention: plugins/PLG_NAME/public/assets/javascript/PLG_NAME.js plugins/PLG_NAME/public/assets/css/PLG_NAME.css

At Runtime

In any place of your controller method add the following line: require_javascript('WhateverYorJsIsNamed.js', $this→plugin_name); Sometimes you want to include JS libraries only on some user actions, so this way is better if that is your case.

If you don't see the 'Plugins' icon in the Administration panel, then enable it by editing 'config/config.php' and inserting the line:

define('PLUGIN_MANAGER', true);
  • Close and restart Feng Office
  • Navigate to Adminstration → Plugins. You should now see an entry for 'helloworld' and an 'Install' link just below.
  • Manually edit the default theme stylesheet to include the class for the tab icon. (See Configuring the Tab Icon above)
  • Click the 'Install' link and, if successful, click the 'Activate' link.
  • Now refresh the browser (Ctrl+F5) and you should see your new tab “World” appear. If not, check the tab ordering for conflicts (Administration → Tabs) and retry.

To move the plugin to another instance of Feng Office simply:

  • Upload the helloworld folder to the new server
  • Make sure the plugin manager is enabled
  • Manually edit the default theme CSS stylesheet
  • Navigate to Administration → Plugins and 'Install' and 'Activate'
  • If necessary, set the tab order in Administration → Tabs
  • Refresh the browser

Note on Installing: With each plugin we may supply scripts that run at install time only. They are placed in the 'plugins/PLUGIN_NAME/install/' folder and invoked in order: 'sql/mysql_schema.php', 'sql/mysql_initial_data.php' and 'install.php'.

Note on Uninstalling. With each plugin we should supply a script called 'plugins/PLUGIN_NAME/uninstall.php', which is invoked by the Plugin Manager and whose function it is to remove the plugin's data and files.

The Uninstall mechanism is not covered in this tutorial, and for now we will manually uninstall:

  • Uncheck the tab in Administration → Tabs
  • 'Deactivate' and 'Uninstall' in Administration → Plugins
  • Remove your plugin folder from the plugins folder
  • Launch phpMyAdmin and delete the relevant records in the 'plugins', 'tab_panels' and 'tab_panel_permissions' tables.

Example with the Email Plugin

In this case we will take the Email plugin as an example provided that anyone can access its source code. The plugin name is 'mail'.

Create a folder named 'PLUGIN_NAME' under FENGOFFICE_ROOT/plugins.

All the necessary resources and code for your plugin will be placed here: javascripts, css, images, php (templates, views, controllers, models).

The first file you need to create is the plugin metadata 'info.php'. Create this file under you plugin root folder 'plugins/PLUGIN_NAME/info.php' containing this php code:

<?php return array(
        "name" => "mail", //name of the plugin
	"version" => "1", //version of the plugin
	"author" => "Feng Office", //author
	"website" => "http://fengoffice.com", //website of the author
	"description" => "Email web client", //brief description of the plugin
	"dependences" => array('core_dimensions'), //if the plugin depends on another plugin to work correctly
	"order" => 1
);?>

This will provide information about the plugin version, description, order, dependences, category, author and other metadata necessary for third party plugin management.

If you want to create a new tab or associate it with a content object, you have to specify to the metadata file some extra configuration as shown below:

<?php return array(
    "name" => "mail",
	"version" => "2",
	"author" => "Feng Office",
	"website" => "http://fengoffice.com",
	"description" => "Email web client",
	"dependences" => array('core_dimensions'),// Array of plugin names (TODO check dependences)
	"order" => 1,
        "types" => array ( //association with a content object
		array(
			"id" => 22, //id of the content object
			"name" => "mail", //content object name
			"handler_class" => "MailContents", //name of the handler class
			"table_name" => "mail_contents", //name of the table containing it
			"type" => "content_object", 
			"icon" => "mail", //icon of the content object
			
		)
	),
    "tabs" => array ( //tab creation
                array(
                        "id" => "mails-panel", //plugin id
                        "ordering" => 2, //order of the tab when shown among the rest, 0 being the first
                        "title" => "email tab", //lang that will be used to specify the tab name
                        "icon_cls" => "ico-mail",  //icon that the tab will have
                        "refresh_on_context_change" => true, 
                        "default_controller" => "mail", //name of the controller 
                        "default_action" => "init" , //first action it will do when loading 
                        "initial_controller" => "" , 
                        "initial_action" => "" ,
                        "type" => "plugin", //type
                        "object_type_id" => 22 //id of the content object if it is related to one
                )
     )
);?>        

This tells the installer that it will have to create a new tab in the system, specifying id, title and many other configuration options, but the most important ones are default_controller and default_action.

Create the folder 'application/controllers' under your plugin structure - so that it looks like FENG_ROOT/plugins/PLUGIN_NAME/application/controllers -, and create the following class under this folder:

Name of the class: [PLUGIN_NAME]Controller.class.php ⇒ i.e.: MailController.class.php

<?php class MailController extends ApplicationController {                
        var $plugin_name = "mail"; //name of the plugin
 
        function __construct() {
                parent::__construct();
                prepare_company_website_controller($this, 'website');
                Env::useHelper('MailUtilities.class', $this->plugin_name); //in case it needs an extra class to handle some functions
		require_javascript("AddMail.js",  $this->plugin_name); //in case it requires a JavaScript file to load certain data
        }

        function init() {
                //do something
        }
}?>

New Plugin with new object type

In this example we are creating the “objectives” plugin with the new object type “Objective”. The plugin needs to have the following structure:

The first step is to build the installer script to: - create new tables - add more columns to existing tables That has to be done in the file

install/sql/mysql_schema.php

Example:

CREATE TABLE IF NOT EXISTS `<?php echo $table_prefix ?>objectives` (
  `object_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `description` text NOT NULL,
  PRIMARY KEY (`object_id`)
) ENGINE=<?php echo $engine ?> <?php echo $default_charset ?>;

Note: for each object in this table we need to add a new record in “objects” table with the same id.

To add new object type to the system then the sql queries will be placed in

install/sql/mysql_initial_data.php

Here we need to add the new object type in “object_types” table:

</code> INSERT INTO `<?php echo $table_prefix ?>object_types` (`name`,`handler_class`,`table_name`,`type`,`icon`,`plugin_id`) VALUES ('objective', 'Objectives', 'objectives', 'content_object', 'objective', (SELECT `id` FROM `<?php echo $table_prefix ?>plugins` WHERE `name`='objectives')) ON DUPLICATE KEY UPDATE id=id; </code>

Then we need to specify that this new object can be classified in the workspaces and tags dimensions:

INSERT INTO `<?php echo $table_prefix ?>dimension_object_type_contents` (`dimension_id`,`dimension_object_type_id`,`content_object_type_id`, `is_required`, `is_multiple`)
    SELECT
     (SELECT `id` FROM `<?php echo $table_prefix ?>dimensions` WHERE `code`='workspaces'), `id`, (SELECT `id` FROM `<?php echo $table_prefix ?>object_types` WHERE `name`='objective'), 0, 1
    FROM `<?php echo $table_prefix ?>object_types` WHERE `name`='workspace'
ON DUPLICATE KEY UPDATE dimension_id=dimension_id;

INSERT INTO `<?php echo $table_prefix ?>dimension_object_type_contents` (`dimension_id`,`dimension_object_type_id`,`content_object_type_id`, `is_required`, `is_multiple`)
    SELECT
     (SELECT `id` FROM `<?php echo $table_prefix ?>dimensions` WHERE `code`='tags'), `id`, (SELECT `id` FROM `<?php echo $table_prefix ?>object_types` WHERE `name`='objective'), 0, 1
    FROM `<?php echo $table_prefix ?>object_types` WHERE `name`='tag'
ON DUPLICATE KEY UPDATE dimension_id=dimension_id;''

Add the new tab to the system, so we can list the objects


INSERT INTO `<?php echo $table_prefix ?>tab_panels` (`id`,`title`,`icon_cls`,`refresh_on_context_change`,`default_controller`,`default_action`,`initial_controller`,`initial_action`,`enabled`,`type`,`ordering`,`plugin_id`,`object_type_id`) VALUES 
 ('objectives-panel', 'objectives', 'ico-objectives', 1, 'objective', 'init', '', '', 1, 'system', 11, (SELECT `id` FROM `<?php echo $table_prefix ?>plugins` WHERE `name`='objectives'), (SELECT id FROM <?php echo $table_prefix ?>object_types WHERE name='objective')) 
ON DUPLICATE KEY UPDATE id=id;

Define permissions for the new tab and for the new object type for each user role


INSERT INTO `<?php echo $table_prefix ?>tab_panel_permissions` (`permission_group_id`, `tab_panel_id`)
 SELECT `c`.`permission_group_id`, 'objectives-panel'
 FROM `<?php echo $table_prefix ?>contacts` `c` WHERE `c`.`user_type` IN (
    SELECT `id` FROM `<?php echo $table_prefix ?>permission_groups` WHERE `type`='roles' AND `name` IN ('Super Administrator','Administrator','Manager','Executive')
 )
ON DUPLICATE KEY UPDATE tab_panel_id=tab_panel_id;

INSERT INTO <?php echo $table_prefix ?>max_role_object_type_permissions (role_id, object_type_id, can_delete, can_write)
SELECT p.id, o.id, 1, 1
FROM `<?php echo $table_prefix ?>object_types` o JOIN `<?php echo $table_prefix ?>permission_groups` p
WHERE o.`name` IN ('objective')
     AND p.`name` IN ('Super Administrator','Administrator','Manager');

INSERT INTO <?php echo $table_prefix ?>max_role_object_type_permissions (role_id, object_type_id, can_delete, can_write)
SELECT p.id, o.id, 0, 1
FROM `<?php echo $table_prefix ?>object_types` o JOIN `<?php echo $table_prefix ?>permission_groups` p
WHERE o.`name` IN ('objective')
     AND p.`name` IN ('Executive');

INSERT INTO <?php echo $table_prefix ?>role_object_type_permissions (role_id, object_type_id, can_delete, can_write)
SELECT p.id, o.id, 1, 1
FROM `<?php echo $table_prefix ?>object_types` o JOIN `<?php echo $table_prefix ?>permission_groups` p
WHERE o.`name` IN ('objective')
    AND p.`name` IN ('Super Administrator','Administrator','Manager');

INSERT INTO <?php echo $table_prefix ?>role_object_type_permissions (role_id, object_type_id, can_delete, can_write)
SELECT p.id, o.id, 0, 1
FROM `<?php echo $table_prefix ?>object_types` o JOIN `<?php echo $table_prefix ?>permission_groups` p
WHERE o.`name` IN ('objective')
    AND p.`name` IN ('Executive');

-- add permissions to users where the users already have permissions (copy permissions from tasks)
INSERT INTO <?php echo $table_prefix ?>contact_member_permissions (permission_group_id, member_id, object_type_id, can_delete, can_write)
 SELECT cmp.permission_group_id, cmp.member_id, (SELECT `id` FROM `<?php echo $table_prefix ?>object_types` WHERE `name`='objective'), cmp.can_delete, cmp.can_write 
 FROM <?php echo $table_prefix ?>contact_member_permissions cmp
 WHERE cmp.object_type_id = (SELECT `id` FROM `<?php echo $table_prefix ?>object_types` WHERE `name`='task')
ON DUPLICATE KEY UPDATE <?php echo $table_prefix ?>contact_member_permissions.member_id=<?php echo $table_prefix ?>contact_member_permissions.member_id;

INSERT INTO <?php echo $table_prefix ?>tab_panel_permissions (permission_group_id, tab_panel_id)
 SELECT pg.id, 'objectives-panel' FROM <?php echo $table_prefix ?>permission_groups pg WHERE pg.type='roles' AND pg.name IN ('Super Administrator','Administrator','Manager','Executive')
ON DUPLICATE KEY UPDATE tab_panel_id=tab_panel_id;

To implement model, views and controller you can take a look at how it is done for the “Notes” module. Controller:

application/controllers/MessageController.class.php

Views:

application/views/webpage

Models:

application/models/project_messages

Important: - The controller must extend the class ApplicationController - The model base classes must extend the classes ContentDataObjects and ContentDataObject

The CSS files must be placed under

plugins/objectives/public/assets/css

The css file with the same name of the plugin will be included automatically at startup, you can add more files and import them in the

plugins/objectives/public/assets/css/<plugin_name>.css

The javascript files must be placed under 


plugins/objectives/public/assets/javascript

The js file with the same name of the plugin will be included at startup In that file you need to create an object with below “og” and the same name of the plugin If you add the “init” function, it will be executed at startup, this is used to make some initializations if needed. Example:
plugins/objectives/public/assets/javascript/objectives.js

og.objectives = {
    init: function() {
        // your code here
    }
}

To build the list of objects to show in the tab you can create a new js file registering a new grid class. As an example you can see how it is done for “Notes” module

public/assets/javascript/og/MessageManager.js

See init function at

application/controllers/MessageController.class.php

to know how to include it