Developers/Concepts/ApplicationHowto
Contents |
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:
First steps
Application Directory Structure
To create a new application you need to create the following directories:
Application/Backend/ Application/css/ Application/Frontend/ Application/js/ Application/Model/ Application/Setup/ Application/translations/
Or you can use the skeleton application 'ExampleApplication' from the svn repository:
https://svn.tine20.org/svn/trunk/tine20/ExampleApplication
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 => false, 'presence'=>'required'),
// add all other fields here and define validation rules
);
}
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';
$this->_currentAccount = Tinebase_Core::getUser();
}
/**
* 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.
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';
/**
* Returns all JS files which must be included for this app
*
* @return array Array of filenames
*/
public function getJsFilesToInclude()
{
return array(
'Application/js/Application.js',
'Application/js/Models.js',
'Application/js/GridPanel.js',
'Application/js/EditDialog.js',
);
}
}
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 = Tine.Tinebase.widgets.app.MainScreen;
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',
});
/*** 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.initFilterToolbar();
this.plugins = this.plugins || [];
this.plugins.push(this.filterToolbar);
Tine.Application.RecordGridPanel.superclass.initComponent.call(this);
},
initFilterToolbar: function() {
this.filterToolbar = new Tine.widgets.grid.FilterToolbar({
filterModels: [
{label: 'Record', field: 'query', operators: ['contains']},
],
defaultFilter: 'query',
filters: []
});
},
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
todo: add css file + description
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 ).




