9. Data Models

9.1. Model-based architecture

A data model encapsulates data, providing a way for the interested parties to query, load and modify said information and get notified in case of changes. The WebApp framework contains a global model that provides an API for handling plugins and insertion points, the hierarchy model (store/folder hierarchy), and settings.

The Ext JS library provides a way of working with server-backed data stores and standard UI components. Most notably, the data grid component (i.e. data representing a list of mails, appointments, etc.) integrates with them. The framework contains base classes for creating such data stores that use the PHP code as backend.

The global model stores information required across contexts and plugins. A schematic overview is shown in “Global data model”.

Global data model

Global data model

A global instance of the Zarafa.core.Container object, simply called container, maintains a list of the registered plugins, lazily-created instances of Zarafa.core.HierarchyModel, and Zarafa.core.SettingsModel.

The hierarchy model is a separate context, see Hierarchy model. For more information, refer to the API documentation on the HierarchyContext class and other classes in the same module.

The settings model is also a separate context, see Settings model. For more information on settings, refer to the API documentation on the SettingsContext class and other classes in the same module.

9.2. Separable model architecture

To promote code re-use and separation of concepts, a separable model architecture is used. In essence, this is just a MVC (Model-View-Controller) design with the view and controller collapsed into a single entity. The responsibility of the model is to store and validate data, and synchronize with the server. On the other side, the view/controller components present the data to the user, allow him to edit the data, and generally drive the model. The view/controller components contain a reference to one or more model objects. They interact with the models with method calls, and get notified of changes through events. This principle is shown in “Separable model architecture”.

Separable model architecture

Separable model architecture

Since this design decouples data manipulation and client/server communication from the user interface, the model part of the application can be easily unit tested outside the browser.

View/controller components never need communicating with each other, or even need to know of each others existence, as all data requests and modifications are done through the models. A concrete example of how models and view/controllers interact is shown in “Deleting a message item from a data store: the ContextMenu deletes item in the model” and on. Shown at the top is a store containing a set of message items, representing the model. Three UI components act as view/controllers, mutating and displaying information in the store.

Deleting a message item from a data store: the ContextMenu deletes item in the model

Deleting a message item from a data store: the ContextMenu deletes item in the model

Deleting a message item from a data store: the model commits the change

Deleting a message item from a data store: the model commits the change

Deleting a message item from a data store: the model fires a delete event on completion

Deleting a message item from a data store: the model fires a delete event on completion

In this example, if a user deletes an item, the ContextMenu object calls the delete method on the store, and then calls save (“Deleting a message item from a data store: the ContextMenu deletes item in the model”) to tell the store to synchronise with the server (“Deleting a message item from a data store: the model commits the change”). When this (asynchronous) operation has finished, a delete event is fired to notify the three components that an item was removed (“Deleting a message item from a data store: the model fires a delete event on completion”). This in turn causes the grid and tool bar to update and reflect the change. Refer to Communication for more detailed information on how the change is communicated to the server.

9.3. Hierarchy model

The HierarchyModel is a model class containing a structured tree for all MAPI stores and MAPI folders in the application. It can be used to receive entryid, store_entryid or parent_entryid of any of its contained elements, but to receive the data from these stores, you should use context models.

Loading the hierarchy from the server can be done by calling the load method.

// Load the folder hierarchy.
container.getHierarchyModel().load();

This method triggers an asynchronous HTTP request. When the hierarchy has loaded (or when an error occurred) the model fires the load event.

// Hook the load event.
container.getHierarchyModel().on('load', function() {
        alert('Hierarchy loaded!');
});

// Load the folder hierarchy.
container.getHierarchyModel().load();

Once the hierarchy has been successfully loaded, it can be queried for stores, folders, default folders, etc:

//Get the calendar folder, used in the Facebook plugin
var calendarFolder = container.getHierarchyStore().getDefaultFolder('calendar');

// in convertToAppointmentRecord, we create a new record of appointment type
// so we need to set parent_entryid of this new record to entryid of the needed folder
// and store_entryid of this new record to store_id, which is store id hierarchy
var calendarRecord = facebookEventRecord.convertToAppointmentRecord(calendarFolder);

9.4. Settings model

The settings model is a tree of key/value pairs that is saved to the server. It is loaded once, when the application loads; however, writes are always committed immediately. All plugin developers should implement a settings module to enable and disable their plugins or widgets.

There is a separate settings page for plugins that becomes visible if you use the zdeveloper plugin (see Zdeveloper plugin). Here, you can let the user configure any options in your plugin, if the need arises to make something configurable. For widgets, the recommended approach is to create a config method (see Widget configuration for an explanation and example).

If you want your plugin to be enabled/disabled by default you may create a config.php file to set default values for your plugin settings.

Here is an example from the Facebook plugin’s config.php file:

/** Disable the facebook plugin for all clients */
define('PLUGIN_FACEBOOK_USER_DEFAULT_ENABLE', false);

PLUGIN_FACEBOOK_ENABLE is used in the injectPluginSettings function in plugin.facebook.php; here are the contents of that file:

/**
 * Facebook Plugin
 * Integrates Facebook events in to the Zarafa calendar
 */
class Pluginfacebook extends Plugin {

        /**
         * Constructor
         */
        function Pluginfacebook() {}

        /**
         * Function initializes the Plugin and registers all hooks
         * @return void
         */
        function init() {
                $this->registerHook('server.core.settings.init.before');
        }

        /**
         * Function is executed when a hook is triggered by the PluginManager
         * @param string $eventID the id of the triggered hook
         * @param mixed $data object(s) related to the hook
         * @return void
         */
        function execute($eventID, &$data) {
                switch($eventID) {
                        case 'server.core.settings.init.before' :
                                $this->injectPluginSettings($data);
                        break;
                }
        }

        /**
         * Called when the core Settings class is initialized and ready to accept
         * the sysadmin's default settings. Registers the sysadmin defaults
         * for the Facebook plugin.
         * @param Array $data Reference to the data of the triggered hook
         */
        function injectPluginSettings(&$data) {
                $data['settingsObj']->addSysAdminDefaults(Array(
                        'zarafa' => Array(
                                'v1' => Array(
                                        'plugins' => Array(
                                                'facebook' => Array(
                                                        'enable' => PLUGIN_FACEBOOK_ENABLE,
                                                )
                                        )
                                )
                        )
                ));
        }
}

An example from the Spreed plugin is the default meeting duration. When creating the time panel for the “Setup Spreed meeting” dialog, we set predefined values for the Spreed meeting duration. The duration is retrieved by getting the default value from calendar default appointment period. See “Settings of calendar”.

Settings of calendar

Settings of calendar

The implementation is shown here:

var duration = container.getSettingsModel().get('zarafa/v1/contexts/calendar/default_appointment_period');

To add your plugin settings to the Zarafa settings model, the injectPluginSettings function should be implemented on the server-side:

/**
 * Called when the core Settings class is initialized and ready to accept
 * the sysadmin's default settings. Registers the sysadmin defaults
 * for the Spreed plugin.
 * @param Array $data Reference to the data of the triggered hook
 */
function injectPluginSettings(&$data) {
        $data['settingsObj']->addSysAdminDefaults(Array(
                'zarafa' => Array(
                        'v1' => Array(
                                'plugins' => Array(
                                        'spreed' => Array(
                                                'enable' => PLUGIN_SPREED_USER_DEFAULT_ENABLE,
                                                'default_timezone' => date_default_timezone_get(),
                                        )
                                )
                        )
                )
        ));
}

You can add any other settings as you wish. For example, again in the Spreed plugin:

'spreed' => Array(
        'enable' => PLUGIN_SPREED_ENABLE,
        'default_timezone' => PLUGIN_SPREED_DEFAULT_TIMEZONE,
        'spreed_setup' => Array(
                'width' => 940,
                'height' => 480,
        )
)

Next to enable, there are settings for the default time zone (default_timezone) and the size of the dialog. The default time zone is taken from the value that is defined in config.php (in this case, the WebApp global one, not the one distributed by the plugin):

/** Default timezone of the spreed meeting  */
define('PLUGIN_SPREED_DEFAULT_TIMEZONE', 'Europe/Amsterdam');

Obviously, if you have something to configure on the server side, the names can differ but the functionality should be the same.

In the end, the result looks something like in “Plugin settings”.

Plugin settings

Plugin settings

Finally, the settings model also supports simple get and set functions for getting and setting values. For quick access to your configuration values, the path to a value is delimited with the forward slash character (/).

// Read flag to 2 seconds.
container.getSettingsModel().set('zarafa/v1/contexts/mail/readflag_time', 2);

More information about the settings model can be found in the API documentation of the SettingsModel class.

If you have survived until here and have written a plugin that does something with the usual interfaces, but are hungry for more, then the upcoming chapters are where you will find all the nitty-gritty details of WebApp.

We dive into MAPI, the communication protocol between WebApp and the server. This will require an understanding of how Zarafa works and how data is stored in MAPI. Finally, a few subjects such as the ant-based build system and server-side translations are handled.