Developers/Concepts/ApplicationHowto

From Tine 2.0 - Wiki

< Developers


Howto: Build an Application

Before we start, you should read the other articles in the Developers/Concepts category.

This step by step HowTo explains the creation of a new Tine 2.0 Application.

You have to replace the 'Application' placeholder with the name of your app.

This chart shows the classes and dependencies between them:

Applayout.png

First steps

Application Directory Structure

To create a new application you need to create the following directories:

 Application/Backend/
 Application/Controller/
 Application/css/
 Application/Frontend/
 Application/js/
 Application/Model/
 Application/Setup/
 Application/translations/


Or you can use the skeleton application 'ExampleApplication' from the GIT repository:

http://git.tine20.org/git?p=tine20;a=tree;f=tine20/ExampleApplication;hb=HEAD

Setup XML

The next step is to create and edit the Application/Setup/setup.xml file. This file is needed to create the application tables.

 <?xml version="1.0" encoding="utf-8"?> 
 <application>
   <name>Application</name>
   <version>0.1</version>
   <order>50</order>
   <tables>
       <table>
           <name>application_table</name>
           <version>1</version>
           <declaration>
                <field>
                   <name>id</name>
                   <type>text</type>
                   <length>40</length>
                   <notnull>true</notnull>
               </field>
               <field>
                 [..more fields...]
               </field>
              <index>
                   <name>id</name>
                   <primary>true</primary>
                   <field>
                       <name>id</name>
                   </field>
               </index>
           </declaration>
       </table>
       [...more tables...]
   </tables>
 </application>

See Setup Backend (Database schema definition) for further explanations on the xml specification.

Initialize.php

The next step is to create the Application/Setup/Initialize.php file. This file is needed to initialize the application during the installation proceess. The standard initialization sets up the ACL rights for your application and can be extended by overriding Application_Setup_Initialize::_initialize. However, don't forget to call parent::_initialize.

If you do not need any special application initialization the class Application_Setup_Initialize can be empty and just needs to extend Setup_Initialize.

 class Application_Setup_Initialize extends Setup_Initialize{}

Backend

Models

Now its time to add the first model to our new application (Record should be renamed to describe the data).

File Application/Model/Record.php:

<?php
/**
* class to hold record data
* 
* @package     Application
*/
class Application_Model_Record extends Tinebase_Record_Abstract
{  
   /**
    * key in $_validators/$_properties array for the field which 
    * represents the identifier
    * 
    * @var string
    */    
   protected $_identifier = 'id';    
   
   /**
    * application the record belongs to
    *
    * @var string
    */
   protected $_application = 'Application';
   
   /**
    * list of zend validator
    * 
    * this validators get used when validating user generated content with Zend_Input_Filter
    *
    * @var array
    */
    protected $_validators = array(
        'id'                    => array(Zend_Filter_Input::ALLOW_EMPTY => true),
        'name'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
        'status'                => array(Zend_Filter_Input::ALLOW_EMPTY => true),
        'container_id'          => array(Zend_Filter_Input::ALLOW_EMPTY => false, 'presence'=>'required'),
    // modlog information
        'created_by'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
        'creation_time'         => array(Zend_Filter_Input::ALLOW_EMPTY => true),
        'last_modified_by'      => array(Zend_Filter_Input::ALLOW_EMPTY => true),
        'last_modified_time'    => array(Zend_Filter_Input::ALLOW_EMPTY => true),
        'is_deleted'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
        'deleted_time'          => array(Zend_Filter_Input::ALLOW_EMPTY => true),
        'deleted_by'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
    // relations (linked Application_Model_Record records) and other metadata
        'relations'             => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
        'tags'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),    
        'notes'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true),
   );
   
    /**
     * name of fields containing datetime or an array of datetime information
     *
     * @var array list of datetime fields
     */    
    protected $_datetimeFields = array(
        'creation_time',
        'last_modified_time',
        'deleted_time'
    );
   
    /**
     * overwrite constructor to add more filters
     *
     * @param mixed $_data
     * @param bool $_bypassFilters
     * @param mixed $_convertDates
     * @return void
     */
    public function __construct($_data = NULL, $_bypassFilters = false, $_convertDates = true)
    {
        // do something here if you like (add default empty values, ...)
        
        return parent::__construct($_data, $_bypassFilters, $_convertDates);
    }
   
    /**
     * fills a record from json data
     *
     * @param string $_data json encoded data
     * @return void
     */
    public function setFromJson($_data)
    {
        parent::setFromJson($_data);
       
        // do something here if you like
   } 
  
}

When searching for records of your application, you need to implement a filter in which the fields to search for are defined.

File Application/Model/RecordFilter.php:

<?php
/**
* filter Class
* @package     Application
*/
class Application_Model_RecordFilter extends Tinebase_Model_Filter_FilterGroup
{
   /**
    * @var string application of this filter group
    */
   protected $_applicationName = 'Application';
   
   /**
    * @var array filter model fieldName => definition
    */
   protected $_filterModel = array(
       'id'             => array('filter' => 'Tinebase_Model_Filter_Id'),
       'query'          => array('filter' => 'Tinebase_Model_Filter_Query', 'options' => array('fields' => array(/* add fields to search for here */))),
       // add more filters if needed
   );
   

Backend classes

If you use a sql database to store your data, you can extend a handy sql backend abstract class. It has a lot of useful functions (like get, search, update, delete).

This backend class could look like this (and perhaps thats already all you need).

File Application/Backend/Record.php:


/**
* backend for records
*
* @package     Application
* @subpackage  Backend
*/
class Application_Backend_Record extends Tinebase_Backend_Sql_Abstract
{
   /**
    * Table name without prefix (required)
    *
    * @var string
    */
   protected $_tableName = 'application_table';
   
   /**
    * Model name (required)
    *
    * @var string
    */
   protected $_modelName = 'Application_Model_Record';
}


Controller

For each model in your application you need one controller. This controller should extend the abstract record controller Tinebase_Controller_Record_Abstract. In the constructor you need to define the backend class, model and application name.

Here is an example (File Application/Controller/Record.php):

/**
* record controller class for Application
* 
* @package     Application
* @subpackage  Controller
*/
class Application_Controller_Record extends Tinebase_Controller_Record_Abstract
{
   /**
    * the constructor
    *
    * don't use the constructor. use the singleton 
    */
   private function __construct() {        
       $this->_applicationName = 'Application';
       $this->_backend = new Application_Backend_Record();
       $this->_modelName = 'Application_Model_Record';
   }    
   
   /**
    * holdes the instance of the singleton
    *
    * @var Application_Controller_Record
    */
   private static $_instance = NULL;
   
   /**
    * the singleton pattern
    *
    * @return Application_Controller_Record
    */
   public static function getInstance() 
   {
       if (self::$_instance === NULL) {
           self::$_instance = new Application_Controller_Record();
       }
       
       return self::$_instance;
   }        
}

Frontend

Json Frontend

With the new abstract json frontend, it is very simple to create a json frontend server for your application. This class represents the public API of your application and is used for the AJAX requests from the JavaScript frontend.

For the basic functionality, all you need is to set the application name in the constructor and define the basic functions for search/delete/get/save + model name in your class. These functions call the generic function in the Tinebase_Frontend_Json_Abstract class.

Note : the phpdoc matter as it is used by the reflection mechanism of zend json server.

Here is an example (File Application/Frontend/Json.php):

/**
*
* This class handles all Json requests for the application
*
* @package     Application
* @subpackage  Frontend
*/
class Application_Frontend_Json extends Tinebase_Frontend_Json_Abstract
{    

    /**
    * controller
    *
    * @var Application_Controller_Record
    */
   protected $_recordController = NULL;

   /**
    * the constructor
    *
    */
   public function __construct()
   {
       $this->_applicationName = 'Application';
       $this->_recordController = Application_Controller_Record::getInstance();
   }

   /**
    * Search for records matching given arguments
    *
    * @param string $filter json encoded
    * @param string $paging json encoded
    * @return array
    */
   public function searchRecords($filter, $paging)
   {
       return $this->_search($filter, $paging, $this->_recordController, 'Application_Model_RecordFilter');
   }     
   
   /**
    * Return a single record
    *
    * @param   string $uid
    * @return  array record data
    */
   public function getRecord($uid)
   {
       return $this->_get($uid, $this->_recordController);
   }

   /**
    * creates/updates a record
    *
    * @param  string $recordData
    * @return array created/updated record
    */
   public function saveRecord($recordData)
   {
       return $this->_save($recordData, $this->_recordController, 'Record');
   }
    
   /**
    * deletes existing records
    *
    * @param string $ids 
    * @return string
    */
   public function deleteRecords($ids)
   {
       $this->_delete($ids, $this->_recordController);
   }   
}

Http Frontend

/**
* This class handles all Http requests for the application
*
* @package     Application
* @subpackage  Frontend
*/
class Application_Frontend_Http extends Tinebase_Frontend_Http_Abstract
{
   protected $_applicationName = 'Application';
}


JavaScript

For the javascript frontend, several files are needed. If you don't want to use the generic tree panel, grid and edit dialog, you can define your own stuff here.

Application.js

Here you define the basic mainscreen and the tree panel of your application.

Ext.namespace('Tine', 'Tine.Application');

// default mainscreen
Tine.Application.MainScreen = Ext.extend(Tine.widgets.MainScreen, {
    activeContentType: 'Record'
}); 
Tine.Application.TreePanel = function(config) {
   Ext.apply(this, config);
   
   this.id = 'ApplicationTreePanel',
   this.recordClass = Tine.Application.Record;
   Tine.Application.TreePanel.superclass.constructor.call(this);
}
Ext.extend(Tine.Application.TreePanel , Tine.widgets.container.TreePanel);

Tine.Application.RecordBackend = new Tine.Tinebase.widgets.app.JsonBackend({
   appName: 'Application',
   modelName: 'Record',
   recordClass: Tine.Application.Model.Record
});

Models.js

In the models file you define the javascript record model(s). It should have the same fields like the php model.

Ext.namespace('Tine', 'Tine.Application.Model');

Tine.Application.Model.RecordArray = Tine.Tinebase.Model.genericFields.concat([
   { name: 'id' },
   { name: 'container_id' },
   /*** define more fields here ***/
]);

Tine.Application.Model.Record = Tine.Tinebase.Record.create(Tine.Application.Model.RecordArray, {
   appName: 'Application',
   modelName: 'Record',
   idProperty: 'id',
   //titleProperty: 'title',
   recordName: 'Record',
   recordsName: 'Records',
   containerProperty: 'container_id',
   containerName: 'Record list',
   containersName: 'Record lists'
});

Tine.Application.Model.Record.getFilterModel = function() {
   var app = Tine.Tinebase.appMgr.get('Application');
      
   return [ 	
       {label : _('Quick search'), field : 'query', operators : [ 'contains' ]}, 
       {filtertype : 'tine.widget.container.filtermodel', app : app
           , recordClass : Tine.Application.Model.Record}, 
       {filtertype : 'tinebase.tag', app : app} 
   ];
};

/*** add more models here if you need them ***/

GridPanel.js

Ext.namespace('Tine.Application');

Tine.Application.RecordGridPanel = Ext.extend(Tine.Tinebase.widgets.app.GridPanel, {
   // model generics
   recordClass: Tine.Application.Model.Record,
   
   // grid specific
   defaultSortInfo: {field: 'creation_time', direction: 'DESC'},
   gridConfig: {
       loadMask: true,
       autoExpandColumn: 'title'
   },
   
   initComponent: function() {
       this.recordProxy = Tine.Application.RecordBackend;
       
       this.gridConfig.columns = this.getColumns();
       this.filterToolbar = this.filterToolbar || this.getFilterToolbar();
       
       this.plugins = this.plugins || [];
       this.plugins.push(this.filterToolbar);        
       
       Tine.Application.RecordGridPanel.superclass.initComponent.call(this);
   },
    
   getColumns: function(){
       return [
       /*** define your columns here, like this: ***/
       /*{
           id: 'number',
           header: this.app.i18n._("Number"),
           width: 100,
           sortable: true,
           dataIndex: 'number'
       }*/];
   }
}

EditDialog.js

Ext.namespace('Tine.Application');

Tine.Application.RecordEditDialog = Ext.extend(Tine.widgets.dialog.EditDialog, {
   
   /**
    * @private
    */
   windowNamePrefix: 'RecordEditWindow_',
   appName: 'Application',
   recordClass: Tine.Application.Model.Record,
   recordProxy: Tine.Application.RecordBackend,
   loadRecord: false,
   tbarItems: [{xtype: 'widget-activitiesaddbutton'}],
   
   /**
    * overwrite update toolbars function (we don't have record grants yet)
    */
   updateToolbars: function() {
   },
   
   /**
    * returns dialog
    * 
    * NOTE: when this method gets called, all initalisation is done.
    */
   getFormItems: function() {
       return {
           xtype: 'tabpanel',
           border: false,
           plain:true,
           activeTab: 0,
           border: false,
           items:[{               
               title: this.app.i18n._('Record'),
               autoScroll: true,
               border: false,
               frame: true,
               layout: 'border',
               items: [{
                   region: 'center',
                   xtype: 'columnform',
                   labelAlign: 'top',
                   formDefaults: {
                       xtype:'textfield',
                       anchor: '100%',
                       labelSeparator: ,
                       columnWidth: .333
                   },
                   items: [[
                       /*** define your form fields here, like this ***/
                       /* {
                       fieldLabel: this.app.i18n._('Number'),
                       name: 'number',
                       allowBlank: false
                       } */
                       }]] 
               }]
           ]
       };
   }
});

Tine.Application.RecordEditDialog.openWindow = function (config) {
   var id = (config.record && config.record.id) ? config.record.id : 0;
   var window = Tine.WindowFactory.getWindow({
       width: 600,
       height: 400,
       name: Tine.Application.RecordEditDialog.prototype.windowNamePrefix + id,
       contentPanelConstructor: 'Tine.Application.RecordEditDialog',
       contentPanelConstructorConfig: config
   });
   return window;
};

CSS

You are able to add your own CSS. This example shows how to override some styles.

/**
 * File: Example.css
 */
.ExampleApplicationIconCls {
   background-image:url(../../images/oxygen/16x16/actions/document-open-recent.png) !important;
}
.x-btn-medium .ExampleApplicationIconCls {
   background-image:url(../../images/oxygen/22x22/actions/document-open-recent.png) !important;
}
.x-btn-large .ExampleApplicationIconCls {
   background-image:url(../../images/oxygen/32x32/actions/document-open-recent.png) !important;
}

Translations

To create the .po and .mo files, you have to run the language helper script (langHelper.php) with the -u option. Afterwards, you can edit the .po file with poEdit to create the translations in the desired languages ( see: http://www.tine20.org/wiki/index.php/Contributors/Howtos/Translations ).