wCMF
3.6
|
The data model defines the domain classes. In principle the following steps are necessary to to make the domain classes available in the application:
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.
We start with step 1. Assuming we're in the fortunate situation of defining the database tables ourselves. Then we proceed as follows.
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.
Of course it's always possible to write own mappers. These must inherit from PersistenceMapper and implement the abstract methods given there. Since the framework already provides mappers, it's easier to use these as base classes. In case of a relational database it's possible to derive custom mappers from NodeRDBMapper. Then the methods for defining the SQL statements must be implemented.
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].
The framework's PersistenceMapper work with the class Node, a subclass of PersistentObject. Different domain classes are created by specifying the type attribute (Node::getType) and a set of attributes (Node::getValueNames). So the class Node is a generic data container, into which data can be put by the method Node::setValue and retrieved by the method Node::getValue. For most applications this will do.
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.
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.
The example outputs the object ids of all nodes that are descendents of $node.
Views are implemented as HTML pages (defined in the view templates), which typically contain a form, which displays the data to be modified. For programming dynamic parts and to access application data the Smarty template language is used.
By default the views are stored as .tpl files in the directory /application/inlcude/views (see [smarty]). In the directory /wcmf/application/views those views are stored, which the framework uses for its standard application. These are the basis for the programming of custom views.
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:
Controllers execute the user-defined actions. In order to implement custom controllers a class must be derived from the baseclass Controller, which implements the methods Controller::hasView and Controller::executeKernel.
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.
A web application typically consists of several input masks (views), which are used to create, modify and delete data. The application is defined by the actions executable in the individual input masks. Thereby the framework makes no difference between actions used for data handling and those used to navigate or e.g. initiate the export of data.
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:
We name the action editArticle. This name need not to be unique in the whole application. The ActionMapper only requires the name (and the Action Keys defined by the action) to find the next appropriate controller.
In order to display the data the application must know which article is selected. This is exactly defined by it's Object Identifier. The data transfer between the input masks is achieved by the HTTP POST mechanism, i.e. a (hidden) input field must exist, which contains the oid of the article to be displayed. Since for most applications it's often necessary to transfer an oid, the framework defines a standard field oid in each view (see file /wcmf/application/views/formheader.tpl), which can easily be set by the JavaScript function doDisplay (/wcmf/blank/script/common.js).
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>
For details on programming the views see Programming the views.
To determine the controller, which carries out the action, the ActionMapper requires an appropriate entry in the configuration file (see [actionmapping]). If the controllers name is ArticleController, the entry could look like this:
[actionmapping] ??editArticle = ArticleController
Don't forget to introduce the ArticleController in the configuration section [classmapping].
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
The action is executed in the controller - in this example in the ArticleController class. Since the controller should display a view with the article's data, we first must specify that the controller has a view and second the data of the article must be passed to the view.
At first however it must be assured, that the controller receives an oid. This happens in the method Controller::validate, which searches for the entry in the passed data:
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.
After the controller has provided the view with the data, the view can display the data. In our case after the ArticleController has been executed a variable article is known to the view, which matches the article node.
The programming of the views is done in HTML together with the Smarty template language. The file article.tpl could contain the following line:
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.
Rights can be assigned to the execution of actions in the user interface or to the editing of domain classes and individual objects (instances of domain classes). For editing content the framework defines the rights read, modify, delete and create.
The authorization for actions in the user interface is handled by the ActionMapper, for actions concerning the data the PersistenceMapper checks the permissions (and sets objects, for which the right modify is not set, to non-editable (is_editable = false) ). Both classes use the method RightsManager::authorize and generate a fatal error message, if authorization fails (see Error handling). To prevent this the rights can be retrieved directly in order to take appropriate messures:
$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].
If more than one user works with the application at the same time, conflicts can occur, if two users want to edit the same object concurrently. In this case the first user can request a lock on that object, which only allows reading access to succeeding users. This lock will be transfered to all instances loaded in the future as long until the user unlocks the object, which happens automatically upon execution of an action (call to main.php). In the sourcecode this looks as follows:
$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 localization of the application the method Message::get is provided. For a given (english) string this method searches for the required language's version. The language version can either be passed directly with the method call or can be set application wide (see documentation of Message::get).
How the translation is retrieved depends on the parameter usegettext (see [cms]). If it is set to 1, the method uses the PHP function gettext. This in turn makes use of *.mo files in the directory /localeDir/language/LC_MESSAGES - e.g. /locale/de_DE/LC_MESSAGES/main.mo (see documentation of gettext).
If gettext doesn't exist, the language version can also be taken from an associative array named messages_language (e.g. messages_de_DE). This must be defined in a file /localeDir/language/LC_MESSAGES/messages_language.php (e.g. /locale/de_DE/LC_MESSAGES/messages_de_DE.php). The keys of the array are the strings, which are passed to the method Message::get, the values are the corresponding translations.
For making the localization more comfortable some tools are provided in the directory /wcmf/tools/i18n:
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, ...
For debugging und logging output the log4php framework is used. For convenient usage wCMF defines a thin wrapper class called Log. To log a debug message in the category of the current class, just call:
Log::debug($message, __CLASS__);
In the application two types of errors are distinguished:
These errors can be produced in the following ways:
Non-fatal: adding a message by calling Controller::appendErrorMsg makes this message - together with all accumulated messages before and those to follow - available in the next view's $errorMsg variable.
In order to delete old messages the method Controller::setErrorMsg class must be called with an empty string parameter.
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