Howto Create An Application Using IsterUI* Classes

Name

ui tutorial -- Howto write an application using IsterUI* classes.

Overview

With the classes of the package ui it is quite easy to setup a web application. Usually the programmer only has to edit a small number of XML files, write database queries and extend some of the standard framework classes to implement the desired logic. The goal of the framework is to provide classes for almost all common tasks but on the other hand not to restrict the freedom of the devoper. The framework should be useful for any use case as long as it involves a web application.

The architecture is based on views and components. A view is a collection of components and components may contain other components nested to any level. The presentation layer is completely driven by a templating engine. The templates are not restricted to be written in XHTML. It is possible to use other text based formats, especially XML. Each component has its own template and the result of the component's toString() method is provided to the template of the enclosing component. Thus it is possible to arrange components at the presentation level as desired.

The connection to a database system in the backend is transparently executed by a database abstraction layer, currently with drivers available for MySQL, PostgreSQL and the PHP abstraction layer DBX. Nevertheless, the database abstraction of the framework is only at the medium level which means the developer remains free to write his own SQL code. It is very handy--but beyond the scope of this document--to use the IsterSqlFunction class to communicate with the database since this class provides an easy way to use what we call SQL functions, usually known as parameterized queries. This reduces the risk of SQL injection vulnerabilities significantly.

Figure 1. Architecture of a typical framework Application

View bigger version of this figure.

The session is maintained automatically by the IsterUIFramework class. Also access control and checking input for constraints with IsterHttpSecurityPolicy objects is performed. The use of forms is easy done using the class IsterUIConnector. It maps a form to an IsterSqlDataObject in both directions and is created by XML description. Since rendering of these forms is not hard coded but again based on templates, it should be possible to use both XHTML forms and XForms.

The use of the ui classes reduces the time needed for development significantly. On the other hand remains the developer free to customize or extend the standard classes in any way. There is no restriction to any use case nor data model. Since all SQL is written by the developer (with a very small number of exceptions) he may use any features of his DBMS he wants. The Ister PHP Framework has been written to make common things easy and rapid to implement and to keep the freedom of development at any time.

What to do with the IsterUIFramework class

In this Howto we focus on the creation of a simple web application with output language XHTML. First we have to write an index.php file as starting point.

<?php
require_once('IsterUIDescFactory.php');
require_once('IsterUIFramework.php');
// create description
$fac = new IsterUIDescFactory();
$descr = $fac->get(ISTER_UI_FRAMEWORK);
// if you do not need one of the XML classes of the framework
// the following is a little faster and less memory consumptive
// $descr = $fac->get(ISTER_UI_FRAMEWORK, ISTER_UI_USE_NATIVE);
$descr->setSource('framework.xml');
// create framework
$fw = new IsterUIFramework;
// set description
$fw->setDescription($descr);
// execute framework
$fw->run();
?>

For a very simple application this may be nearly all PHP we have to write. A real life example of course would need a little more.

Next we have to write the framework.xml. This is a description of the application.

<?xml version="1.0" ?>
<!DOCTYPE framework SYSTEM "isteruiframework.dtd">
<framework>
    <logging>
        <logger class="IsterLoggerStd"/>
    <logging>
    <meta>
        <application class="IsterAppMain"
                     logrequest="yes" logresponse="yes"/>
        <selector type="pathinfo" default="index"/>
        <configuration type="ISTER_APP_PROPERTY_T_INI" setup="file=test.ini"/>
        <session name="TESTSESSION" 
                 object="container" container="IsterSessionObject"/>
        <protector class="IsterACLProtector" view="login" uidname="identity"/>
    <meta>
    <!-- Here we will add some more nodes later. -->
<framework>
       

The first section to write is the logging section. For short we only use an IsterLoggerStd to write messages to STDOUT but we could add more loggers here to write to a file or into a database.

In the <application> node we define which class the application object shall instantiate. This must always be a subclass of IsterAppMain. The logrequest and logresponse attributes wants the application to log the request when all work has been done and to log some information about the resources the creation of the response has consumed. Now we define by which request object the active view shall be selected. We choose pathinfo and not get or post to make Google happy. A request like http://www.myserver.org/index.php/index would select a view called index which we define in detail later. The view to call if no view has been selected by the request is given with the default attribute. With the <configuration> node we define a class to perform persistent application configuration. Here we use a configuration file in ini format but configuration via the database would also be possible. With the setup attribute we pass some setup information to the IsterAppPropertyCollection created by this node. Then a session is defined, tracked with a session cookie TESTSESSION. The application object will create a session container object named container as an instance of the class IsterSessionObject. Last but not least we setup an object for access control.

In a production environment you will of course disable the standard logger and write your messages elsewhere since the user might not be amused about warnings. Not so the bad guy who will read it and learn everything about the internals of your application.

It is time now to apply a connection to a DBMS.

    <sql class="IsterUITestSqlFunction" driver="IsterSqlDriverPostgresql">
        <function name="testfunction">
            SELECT * FROM test WHERE id='%id%';
        </function>
    </sql>

Since we are professionals we use a PostgreSQL database which may be harder to handle but gives us much more functionality compared to the stable releases of MySQL. The object declared here will be available in each IsterUIView object of the application. If subsequent components have not an own SQL object declared they will inherit this. The <function> node defines an SQL function with the given name which may be called from inside of each object having access to this SQL object. You should add here only those functions really needed by all components. It is possible to add functions for each component later.

There is no connection information added here and it is not possible to do so. The class specified in the class attribute must be able to connect to the database itself. This is for security reasons. Since this is an XML file and it may reside in a directory accessible by the webserver everybody could read the username and password pointing his browser to framework.xml. This feature will never be added. So we have to extend IsterSqlFunction.

<?php
require_once('IsterSqlFunction.php');

class IsterUITestSqlFunction extends IsterSqlFunction {

    function IsterUITestSqlFunction($driver)
    {
        parent::IsterSqlFunction($driver);
    }

    // overwrite connect
    function connect()
    {
        //include a file with connection data or hard code it here
        $this->setConnection('localhost', 'user', 'pass', 'testdb');
        return parent::connect();
    }
}
?>

The remaining step for our framework.xml is to define some views. We will only add one view to keep it simple. How to apply acces control is described in detail below.

    <view name="index" description="index.xml" select="/index"
            protected="no" config="r">
    </view>

The names of the attributes are speaking so we have not that much to explain. The view is not protected what simply means the access control methods will let everybody view this view. The configuration object can only be read from within this view, not written. To describe the logic of the view is part of another XML file index.xml, the value of the description attribute. This file will be described in the following section.

Creating Views

A view is a collection and arrangement of components. Each component has its own template. Components contain other components up to any level. An enclosed component is known to its enclosing component's template by name. This way you may arrange components in a template as you like. In a view components are executed in the order of definition. Inner components are executed prior to their enclosing component. If you think of the arrangement of components as a tree the execution is in "postorder".

To define such an arrangement of components yoe have to edit an XML file, say index.xml for the view "index".

<?xml version="1.0"?>
<!DOCTYPE view SYSTEM "isteruiview.dtd">
<view name="index">
    <component name="design" loc="local" type="class" 
                   class="IsterUIComponent">
        <template lang="t24" file="design.tmpl" path="."/>
        <sql class="IsterUITestSqlFunction" driver="IsterSqlDriverPostgresql">
            <function name="viewtest">SELECT * FROM test;</function>
            <function name="viewother">SELECT * FROM other;</function>
        </sql>
        <component name="content_outer" loc="local" type="class"
                           class="IsterUIComponent">
            <template lang="t24" file="outer.tmpl" path="."/>
            <component name="content_inner" loc="local" type="class"
                                   class="IsterUIComponent">
                <template lang="t24" file="inner.tmpl" path="."/>
            </component>
        </component>
    </component>
</view>

Let's see what we have done. First we have given the view a name. This name should match the name specified in the framework.xml for this view. Then we defined a first component design. This will be a container for all other components. We defined an IsterSqlFunction object with two predefined functions. This object will override the one defined in the framework.xml but it will be passed to all subsequent components as well. Also we defined a template of the framework's default language t24 which reside in the current working directory indicated by the path attribute. This template may look like this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Index</title>
</head>
<body>
<?t24 content_outer ?>
</body>
</html>

This will simply insert the output of the component content_outer at the specified place. This component includes another one called content_inner which is only known to its parent component. The template of the parent may look like this:

<div id="outer">
<h1>content outer</h1>
<?t24 content_inner ?>  
</div>

Last but not least the inner component's template may look like this, for simplicity:

<div id="inner">
<h2>content inner</h2>
</div>

As of version 0.4.0 all components have to be located local as specified by the loc attribute. Future versions may provide remote components such as webservices.

Extending IsterUIComponent

Objects of the class IsterUIComponent does nothing more than executing a given template. If you need more funtionality--and your job is to implement much more--you have to extend this class.

An extension can do anything you want. If a component needs database connectivity you may define a SQL object in the definition of the component or you may inherit such an object from the parent class as described above.

The entry point to extend the class is the run() method. This should return a true value if the template shall be executed afterwards.

<?php
require_once('IsterUIComponent.php');

class IsterUIMyComponent extends IsterUIComponent {

    function IsterUIMyComponent()
    {
        parent::IsterUIComponent();
    }

    // our new run() method adds a "date" variable to the template
    function run()
    {
        $this->tmpl->setProperty('/template', array('date' => date('m.d.y')));
        return true;
    }
}
?>

A component should always return a string in the current ouput language (e.g. XHTML) if the toString() method is called. If you decide to overwrite toString(), don't forget to call parent::toString() and return the returned value if all work has been done.

This component will be defined in the view description as follows:

<component name="date" loc="local" type="class" class="IsterUIMyComponent">
    <template lang="t24" file="date.tmpl" path="."/>
</component>

The class definition of the component must reside in the application's include path.

A specialized component - IsterUIFormConnector

It is one of the most common tasks in web applications to exchange data between a web form and certain database tables. To make this task easier to handle the Ister PHP4 Framework provides the class IsterUIFormConnector. This class represents a connection between a web form and an IsterSqlDataObject. Updates of data in both directions are maintained automatically if the connector is created by an XML description.

Here is a simple example of such a description, say form.xml:

<?xml version="1.0"?>
<!DOCTYPE connection SYSTEM "isteruiconnection.dtd">
<connection>

    <object name="object" class="IsterSqlDataObject" idname="id">
        <sql type="function" action="select" name="test_select"/>
        <sql type="function" action="insert" name="test_insert"/>
        <sql type="function" action="update" name="test_update"/>
        <sql type="function" action="delete" name="test_delete"/>
        <sql type="function" action="mklist" name="mklist">
            SELECT * FROM test;
        </sql>
        <sql type="function" action="none" name="afunction">
            SELECT @last=MAX(id) FROM test;
            SELECT * FROM test WHERE id > (@last - 100);
        </sql>
    </object>

    <form name="testform" class="IsterUIForm">
    
        <action name="insert" call="insert" type="default"/>
        <action name="update" call="update" type="default"/>
        <action name="delete" call="delete" type="default"/>
        
        <field name="oid" type="integer" required="yes">
            <column name="id" type="integer"/>
            <policy type="HTTPSP_T_EXISTS" value="true" 
                    action="HTTPSP_A_PASS" default="0"/>
        </field>
        <field name="input" type="string" required="yes">
            <column name="text" type="integer"/>
            <policy type="HTTPSP_T_REGEXOK" value="/\w+/" 
                    action="HTTPSP_A_MARK" default=""/>
        </field>
        <field name="flag" type="integer" required="yes">
            <policy type="HTTPSP_T_NUM_EQU" value="0" 
                    action="HTTPSP_A_MARK" default="0"/>
        </field>
        
    </form>
        
</connection>

Let's discuss this description.

In the first part we define the data object. It has to have a name and the class must be a subclass of IsterSqlDataObject. The idname attribute specifies the name of the primary key column of the object. A data object does not necessarily represent a dataset of a single table. The data may be spread upon any number of tables. It is up to the different SQL functions to collect the data into a single row. These functions are defined with the following lines. Each sql node defines one SQL function. The action attribute defines for which object action the function shall be called and the name attribute specifies the name of the SQL function to call. This function may either already defined in the current SQL function object or it may be defined in the way shown for the mklist function. The function object has to be passed to the connection from outside, usually from an enclosing component or the view the connection belongs to. It is also possible to add a function not bind to a method of the object (action="none"). This may be used in the overwritten postRun() method of the data object.

If you want to define a multiline SQL function in conjunction with an ISTER_UI_USE_NATIVE description, you must take care about the line separator. The lines of the function are splitted by a regular expression '/;\s+/' but you may note explicitly a '\n' representing a newline behind the ';' to make this unambiguous. Note: do this only for ISTER_UI_USE_NATIVE descriptions!

The next section of the XML file defines the form part of the connection. The form must also have a name but the class of the form must be a subclass of IsterUIForm. Next we define available actions for the form. An action is triggered when the user clicks on a button in the form. The name of the action eaquals to the value attribute of the button in an XHTML form. The call attribute of the descriprion specifies which of the actions defined for the object above should be triggered for this button. The type attribute may be one of "default" or "imgbutton". While the first describes one of the usual form buttons the latter may be used for XHMTL input fields of type "image". For these you may add an additional range attribute.

After the actions are defined the form fields must be declared. Each form field must at least have a name. Further you should specify the type of the input, whether it is "integer", "float" or "string" and if the field is a required field in the form. Inside of a field container you define to which column of the data object the field should be matched. You may also define any number of security policies for a field. Note that if you do not define at least one security policy for a field that field is silently discarded by default.

An IsterUIFormConnector is a component like others. It has to be included in the corresponding description of a view. For our simple example this would look like this:

<component name="form" loc="local" type="form" description="form.xml">
    <template lang="t24" file="form.tmpl" path="."/>
</component>

You may note: the form is not automatically rendered by the connector object. The form data are passed to the template and it is up to the template engine to actually render the form. This way you may output the form as XHTML or XForms, as you like. To the template each field is provided by its name. If the field is required but missing a variable <fieldname>_MISSED will exist with a true value. If it has been marked a variable <fieldname>_MARKED will be defined and set true. The template for our simple form may look like this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <title>Form</title>
</head>
<body>
  <form action="index.php/form" method="post">
    <input type="hidden" name="oid"   value="<?t24 oid ?>"/>
    <input type="text"   name="input" value="<?t24 input ?>"/>
    <?t24 :if input_MISSING ?>
      <p class="error">It is required to input some text.</p>
    <?t24 :end ?>
    <?t24 :if input_MARKED ?>
      <p class="error">Please enter only word charakters.</p>
    <?t24 :end ?>
    <input type="checkbox" name="flag" value="<?t24 flag ?>"/>
    <?t24 :if flag_MARKED ?>
      <p class="error">Please check the checkbox.</p>
    <?t24 :end ?>
    <input type="submit" name="insert" value="Insert">
    <input type="submit" name="update" value="Update">
    <input type="submit" name="delete" value="Delete">
  </form>
</body>
</html>

It is also possible to add groups of form fields evaluating to a single value such as radio buttons. The XML description:

<form name="testform" class="IsterUIForm">
    <group name="gender" required="no" column="gender" type="varchar">
        <field name="gender_female" type="integer" default="1"/>
        <field name="gender_male"   type="integer" />
    </group>
</form>

And the HTML template:

<form action="index.php/form" method="post">
  <input type="radio" name="gender" value="gender_female" 
    <?t24 :if gender_female == 1 ?>checked="checked"</t24> />
  <input type="radio" name="gender" value="gender_male" 
    <?t24 :if gender_male == 1 ?>checked="checked"</t24> />
</form>

Applying access control

As of version 0.4.0 access control is maintained on the level of views. Future versions may also implement access control per component.

To use access control you have to insert some ACL entries into the framework.xml file. This may look like this:

<meta>
    <protector class="IsterACLMyProtector" view="login" uidname="identity"/>
</meta>
<view name="index" description="index.xml" select="/index" 
      protected="yes" config="r">
    <acl type="role" name="all"    access="r"/>
    <acl type="role" name="editor" access="w"/>
</view>
<view name="login" description="login.xml" select="/login"
      protected="no" config="r"/>
<view name="system" description="system.xml" select="/system"
      protected="yes" config="r">
    <acl type="role" name="administrator" access="r"/>
    <acl type="role" name="administrator" access="w"/>
    <acl type="user" name="admin" access="r"/>
    <acl type="user" name="admin" access="w"/>
</view>

For each view a number of acl nodes has been added. Each such node defines an access right. The IsterUIFramework object checks for each request if the current user has the appropriate rights to access the view. If this check fails--for example because the user has not been authenticated before--but an ACL for a role "all" is found, access will be granted nevertheless. In our example the view "index" can be read by everyone whether he/she has been authenticated or not but it can only be written to this view if the current user has the role "editor". The view "login" has no ACL. The "system" view can only be accessed with the role "administrator". The user "admin" which may not come with the "administrator" role can also access this view. While it is possible to enter user based ACLs it is not recommended since it may produce a remarkable overhead for a large number of users.

If access is checked the defined protector class will by default check the session for an object with uidname, in our example "identity". This must be a subclass of IsterACLIdentity. Then it is checked if the identity has an attribute with the name given with the type attribute of the acl element. If the value of the attribute matches one of the values of the name attribute of the acl element access is granted. Therefore your login view must create such an IsterACLIdentity and register this to the current session.

<?php
require_once('IsterACLIdentity.php');

class MyLogin extends IsterUIComponent {

    function MyLogin {
        parent::IsterUIComponent();
    }
    
    function toString() {
        $this->run();
        return parent::toString();
    }
    
    function run() {
        // authenticate the user somehow
        // ...
        
        $uid      =  1;
        $username = 'tester';
        $role     = 'editor';
        
        $id = new IsterACLIdentity($uid, $username);
        $id->setAttribute('role', $role);
        
        // if this is a component inside of a view
        // $this->fw has already been set
        // note: we need a reference here
        $container =& $this->fw->getContainer();
        $container->setAttribute('identity', $id);
    }
}
?>

Sometimes it is useful to overwrite the check() method of IsterACLProtector.

The access methods like "r" for read and "w" for write have to be used by your own classes in an appropriate way. The framework does only restrict access but does not decide what to do for a given access method. This may change in the future.

Some remarks about security

When you setup your environment you should consider to include class files, xml and ini files from a directory above document root. Set your open_basedir configuration variable to such a directory and place all files not called directly by the webserver there.

It is always a good idea to filter incoming data. Use the IsterHttpSecurityPolicy class for this. Data not needed or coming in with wrong format should be discarded.

Links

Other Documents

Table of contents

Figures

Architecture of a typical framework Application