![]() |
![]() |
Basically it doesn't matter, in which format the application data is stored. To load and store data application developers solely have to communicate with the PersistenceFacade. The PersistenceMapper classes have the actual knowledge of how to access the data. This abstraction makes it for instance possible to easily migrate the data storage from file based to databased. Only the mappers have to be exchanged. On the other hand it's also possible - an adequate implementation of the mappers assumed - to access arbitrary database schemes, which eases the connection of the wCMF to existing data sources.
In practice it has been proved as advantageous to use a database as storage medium, because it has a better performance and can handle bigger amounts of data much better than XML files can.
It is recommended to define one table for each domain class in the database scheme (e.g. author, article). On one hand it makes the import and export from and to other applications easier and on the other hand the access to the data is optimized compared to storing all data in one table, because then additional joins would be required to determine data types and other meta information. In addition the table columns can be more easily adjusted to the needs of the domain classes (in an extreme case all data must be stored as BLOB in an universal data table). NodeToSingleTableMapper implements the storage of all data in one table, which - for the said reasons - isn't recommended.
Proceeding on these assumptions the creation of the data model becomes a lot simpler.
Further attributes follow a scheme, which is predefined by the NodeUnifiedRDBMapper. This makes the creation of the mappers easier:
If an N to M relation should be established between two domain classes (tables), a connection table must be defined. This table contains two primary keys, which point to the two connected tables.
As an example serves a domain class Article, which is related to an Author:
CREATE TABLE `Article ` ( `id` int(11) NOT NULL default '0', `fk_author_id` int(11) default NULL, `headline` VARCHAR(255), `text` TEXT, `sortkey` INT(3), PRIMARY KEY (`id`) ) TYPE=MyISAM;
If the database tables already exist, this step is skipped. This however means, that more work must be put into the implementation of the PersistenceMapper.
Assuming the database tables are created by the scheme above, we can proceed to the next step and use NodeUnifiedRDBMapper as baseclass for our mappers. After that we have to implement the methods RDBMapper::getType, NodeRDBMapper::createObject, NodeUnifiedRDBMapper::getTableName, PersistenceMapper::getPkNames, NodeUnifiedRDBMapper::getMyFKColumnNameImpl and NodeUnifiedRDBMapper::getObjectDefinitionImpl. These methods merely ask for properties of the domain class. The example shows the mapper for the table Article mentioned above:
class ArticleRDBMapper extends NodeUnifiedRDBMapper
{
/**
* @see RDBMapper::getType()
*/
function getType()
{
return 'Article';
}
/**
* @see NodeRDBMapper::createObject()
*/
function &createObject($oid=null)
{
return new Article($oid);
}
/**
* @see NodeUnifiedRDBMapper::getTableName()
*/
function getTableName()
{
return 'Article';
}
/**
* @see PersistenceMapper::getPkNames()
*/
function getPkNames()
{
return array('id' => DATATYPE_IGNORE);
}
/**
* @see NodeUnifiedRDBMapper::getMyFKColumnNameImpl()
*/
function getMyFKColumnNameImpl($parentType)
{
// start from the most specific
if ($this->getType() == 'Article' && $parentType == 'Author') return 'fk_author_id';
if ($parentType == 'Author') return 'fk_author_id';
return '';
}
/**
* @see NodeUnifiedRDBMapper::getOrderBy()
*/
function getOrderBy()
{
return array();
}
/**
* @see NodeUnifiedRDBMapper::getObjectDefinitionImpl()
*/
function getObjectDefinitionImpl()
{
$nodeDef = array();
$nodeDef['_properties'] = array
(
array('name' => 'is_searchable', 'value' => false),
);
$nodeDef['_datadef'] = array
(
/*
* Value description:
*/
array('name' => 'id', 'app_data_type' => DATATYPE_IGNORE, 'column_name' => 'id',
'db_data_type' => 'INT(11) NOT NULL', 'default' => '', 'restrictions_match' => '', 'restrictions_not_match' => '',
'restrictions_description' => '', 'is_editable' => false, 'input_type' => 'text', 'display_type' => 'text'),
/*
* Value description:
*/
array('name' => 'fk_author_id', 'app_data_type' => DATATYPE_IGNORE, 'column_name' => 'fk_author_id',
'db_data_type' => 'INT(11)', 'default' => '', 'restrictions_match' => '', 'restrictions_not_match' => '',
'restrictions_description' => '', 'is_editable' => false, 'input_type' => 'text', 'display_type' => 'text'),
/*
* Value description:
*/
array('name' => 'headline', 'app_data_type' => DATATYPE_ATTRIBUTE, 'column_name' => 'headline',
'db_data_type' => 'VARCHAR(255)', 'default' => '', 'restrictions_match' => '', 'restrictions_not_match' => '',
'restrictions_description' => '', 'is_editable' => false, 'input_type' => 'text', 'display_type' => 'text'),
...
);
$nodeDef['_ref'] = array
(
/*
* Value description:
*/
array('name' => 'author_name', 'ref_type' => 'Author', 'ref_value' => 'name', 'ref_table' => 'Author',
'id_column' => 'id', 'fk_columns' => 'fk_article_id', 'ref_column' => 'name')
);
$nodeDef['_parents'] = array
(
array('type' => 'Author', 'is_navigable' => false, 'table_name' => 'Author', 'pk_columns' => array('id'),
'fk_columns' => 'fk_author_id')
);
$nodeDef['_children'] = array
(
array('type' => 'Image', 'minOccurs' => 0, 'maxOccurs' => 'unbounded', 'aggregation' => false, 'composition' => true,
'is_navigable' => false, 'table_name' => 'Image', 'pk_columns' => array('id'), 'fk_columns' => 'fk_article_id',
'order_by' => array())
);
return $nodeDef;
}
}
The method NodeUnifiedRDBMapper::getObjectDefinitionImpl is the most important method. It supplies an associative array, in which the attributes of the domain class (_datadef) the parent and the child domain classes (_children) are defined. In addition the Article contains a reference to the name of the Author (_ref), which allows to access the Author without loading it.
Detailed information on the implemetation can be found under NodeUnifiedRDBMapper::getObjectDefinitionImpl.
For configuring the domain classes in the configuration file see [typemapping].
If however more specialized domain classes are required for the application, they must be created by the PersistenceMapper. In this case subclasses of NodeRDBMapper must override the method NodeRDBMapper::createObject, which in turn creates instances of the special domain class.
$persistenceFacade = &PersistenceFacade::getInstance(); // load model $oid = PersistenceFacade::composeOID(array('type' => 'section', 'id' => array('1'))); $node = &$persistenceFacade->load($oid, BUILDDEPTH_INFINITE); if ($node == null) Message::error("A Node with object id ".$oid." does not exist.";
The example loads the section node with id 1 and all children nodes. If the oid of the requested node is not known either use the appropriate methods of PersistenceFacade (e.g. PersistenceFacade::getOID) or in more complex cases do an ObjectQuery.
$iterator = new NodeIterator($node); while(!($iterator->isEnd())) { $currentObject = &$iterator->getCurrentObject(); Message::trace($currentObject->getOID()); $iterator->proceed(); }
The example outputs the object ids of all nodes that are descendents of $node.
$iterator = new NodeIterator($node); // Output into dot format $filename = "graph.dot"; $dotOS = new DotOutputStrategy($filename); $ov = new OutputVisitor($dotOS); $ov->startIterator($iterator); iterator->reset($node); // Output into XML format $filename = "graph.xml"; $xmlOS = new XMLOutputStrategy($filename); $ov->setOutputStrategy($xmlOS); $ov->startIterator($iterator);
$node->setValue('text', 'hello world!', DATATYPE_ATTRIBUTE); // Saving changes // If one node has been modified $node->save(); // If several connected nodes have been modified $iterator = new NodeIterator($node); $cv = new CommitVisitor(); $cv->startIterator($iterator);
In the view templates all data, which was passed to the view instance is accessible (see Programming the controllers). In the simplest case these can be displayed via {$variable}. In addition object data can be accessed by using {$object->getValue(...)}. By setting debugView = 1 (see [cms]) in the configuration file Smarty will display the data, which is available in the template, in an external window.
The data displayed in the view's form is available to the following controller. Some (hidden) input fields should always exist. They are defined in the file /wcmf/application/views/formheader.tpl, which - to simplify matters - should be reused.
For handling the form data some JavaScript functions are provided (and documented) in the file /wcmf/blank/script/common.js.
In the directory /wcmf/lib/presentation/smarty_plugins the framework defines extensions of the Smarty template language:
The Request instance passed to the Controller::initialize method provides all data of the preceeding view's input fields to the controller. The names of the input fields are the names of the request values. The controller in turn can pass data to the view by setting them on the Response instance.
The method Controller::hasView returns true or false, whether a view is displayed or not (the return value can differ depending on the context or action, for an example see LoginController).
The method Controller::executeKernel executes the actual action. In this method application data is loaded, modified, created, deleted and where required passed to the view for display or to the next controller to proceed. The method either returns false, which means, that the ActionMapper should call no further controller or true. In the latter case the ActionMapper determines the next controller from the context and action values of the response (see Action Keys). This means if a view should be displayed, the method must return false.
While programming custom controllers often the methods Controller::initialize and Controller::validate are overridden in order to carry out initializations or to validate provided data.
The framework's controllers are located in the directory /wcmf/application/controller.
The definition of an action requires the following steps:
As an example we use the action for displaying an article node in order to edit it. Let's look at the individual steps:
The action is triggered upon submission of the input form. Another JavaScript function (submitAction) simplifies the execution. The form data is passed to the main.php script, which delegates the further execution to the ActionMapper. The link to execute the action could look like this:
<a href="javascript:setContext('article'); doDisplay('{$article->getOID()}');
submitAction('editArticle');">{translate text="edit"}</a> [actionmapping] ??editArticle = ArticleController
Additionally the ArticleController should display a view for editing the article. If we name this view article.tpl, the configuration entry would look like the following (see [views]):
[views] ArticleController?? = article.tpl
function validate()
{
if ($this->_request->getValue('oid') == '')
{
$this->setErrorMsg("No 'oid' given in data.");
return false;
}
return true;
} We declare the existence of a view in the method Controller::hasView:
function hasView()
{
return true;
} Finally the action is executed in the method Controller::executeKernel. Here the controller loads the data and provides it to the view for display by setting it on the response instance:
function executeKernel()
{
$persistenceFacade = &PersistenceFacade::getInstance();
// load model
$article = &$persistenceFacade->load($this->_request->getValue('oid'), BUILDDEPTH_INFINITE);
// assign model to view
$this->_response->setValue('article', $article);
// stop processing chain
return false;
} It's important that the method returns false, since this causes the ActionMapper to end the execution and wait for user input. The display of the view is done by the framework.
article name: {$nodeUtil->getInputControl($article, "name")} In the curly brackets you can find Smarty code, which calls the method NodeUtil::getInputControl. This method displays the input control (in our case a textfield), which corresponds to the article's name attribute, in the HTML page. In the same manner the other attributes can be handled.
$rightsManager = &RightsManager::getInstance();
if ($rightsManager->authorize($this->_request->getValue('oid'), '', ACTION_READ))
{
$object = &$persistenceFacade->load($this->_request->getValue('oid'), BUILDDEPTH_INFINITE);
}
else
{
// do something else if the user cannot read the object
} In the example the object is only loaded, if it's permitted. For the definition of rights see [authorization].
$lockManager = &LockManager::getInstance();
$object = &$persistenceFacade->load($this->_request->getValue('oid'), BUILDDEPTH_INFINITE);
// if the object is locked by another user we retrieve the lock to show a message
$lock = $object->getLock();
if ($lock != null)
{
$lockMsg .= $lockManager->getLockMessage($lock, $recipe->getName());
}
else
{
// try to lock object
$lockManager->aquireLock($this->_request->getValue('oid'));
}
The functionality described above is imlemented in the method LockManager::handleLocking, with the only difference that this method only acquires a lock in cases in which the user is allowed to modify the object.
An object, which is loaded by another user is set to non-editable (is_editable = false) automatically upon loading.
For localizing the view templates a Smarty plugin is provided, which is to be used as follows:
{translate text="Logged in as %1%" r0=$authUser->getLogin()} The parameters of the method Message::get are defined by the values of r0, r1, ...
Log::debug($message, __CLASS__);
These errors can be produced in the following ways:
onError($message, $file='', $line='')
Many classes define a method getErrorMsg, which provides more detailed information on the cause of an error.
Back to the Overview | Previous section Database scheme | Next section Howto Start
|
This page generated via doxygen 1.5.8 Mon Mar 30 01:58:37 2009. Copyright © 2009 wemove digital solutions GmbH. |
|