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.
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.
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.
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.
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.
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>
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.
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.