PHP DEVELOPMENT KIT (TUTORIAL)
Table of Contents
01. Introduction
02. Install MySQL, PostgreSQL, Apache and PHP
03. Install the framework. Directory structure explained
04. How the framework works or what happens behind the scene
05. How to deal with errors. The exception handling mechanism
06. Validate input data
07. Internationalization. Add a new language to your website
08. Dealing with data sources
09. Interact with the database - select, insert, update, delete. Call stored procedures
10. Create complex database selects - Criteria API
11. ORM
12. Data repeater
13. Cache, cache & cache
14. How to login users
15. Tips, tricks and best practices
16. Benchmark
01. Introduction
The framework has been created back in 2005. It's first public release was in 2007. The initial goal was to create a platform with plugin support, so all widgets like
data grid, date and time fields, etc. to be outsourced to third party vendors. The chief architect and software engineer of the project was a long time Borland Delphi
developer and he took many ideas from this product. Back in these years ASP.NET was making it's first steps and the data flow principles has been taken from there. Both
of the products had one common architect - Anders Hejlsberg, so they shared a lot of things on architectural level.
02. Install MySQL, PostgreSQL, Apache and PHP
This chapter is optional. If you already have the software installed you can skip reading it.
03. Install the framework. Directory structure explained
PHP Development Kit has very simple installation. For simplicity reasons we will suppose that you are planning to build a site called www.mysite.com
 and you prefer to keep your sites in directory /hosting/web/ for Linux. This directory path is not obligatory and you can change it to whatever you want.
 
## Download the framework and unzip it under this directory
 
cd /hosting/web
mkdir mysite.com && cd mysite.com
wget http://www.phpdek.com/download/files/pdk-2.0.1.tar.bz2
tar jxf pdk-2.0.1.tar.bz2
rm -f pdk-2.0.1.tar.bz2
 
 
## Give read/write permissions to the following files and directories
## Note: ugo+rw gives rights to everyone, you may want to add rights only to a specific user.
 
chmod -R ugo+rw /hosting/web/mysite.com/protected/log/
chmod -R ugo+rw /hosting/web/mysite.com/protected/persistent/
chmod -R ugo+rw /hosting/web/mysite.com/public/www/repository/
 
## in case you are planning to setup http://admin.mysite.com
chmod -R ugo+rw /hosting/web/mysite.com/public/admin/repository/
 
 
## Set the domain in httpd.conf or httpd-vhosts.conf
 
<VirtualHost *:80>
    ServerAdmin webmaster@mysite.com
    DocumentRoot "/hosting/web/mysite.com/public/www"
    ServerName www.mysite.com
    ErrorLog logs/mysite.com-error_log
    CustomLog logs/mysite.com-access_log common
</VirtualHost>
 
That's all. Now you can adjust /hosting/web/mysite.com/config.inc and start placing your pages  under "/hosting/web/mysite.com/public/www/".
This is the directory structure for every website using the framework. You can click on any folder or file to see what functionality it has.
04. How the framework works or what happens behind the scene
Probably instead of drawing charts and dataflow diagrams the best way to explain the conceptions is by showing a basic example. 
If you haven't taken a look so far at any of the source code files under the EXAMPLES section, please do it before continuing
reading this tutorial. If you have done this, then keep reading.
The most basic unit when you create a website is the page. With the framework every page is separated in 2 different files. The first one
has *.php extension and the second one has *.php.script. We keep the presentation logic in the first one and the business logic in the second one.
In a traditional Model-View-Controller architecture the first file is our View and the second is our Controller. We'll see later where is the Model.
For our demo we will use test.php. 
First take a look at the very first line.
 
<?php include "test.php.script"; $page = createPageInstance(); ?>
 
Here we include the Controller and through createPageInstance initialize the class, which will handle the requests. This function is smart enough to find the target
class only if there is only one class included, which extends \web\Page. If you want you can do also createPageInstance("TestPage") In 99% of the cases it is not necessary. During 
initialization TestPage will call many methods like: onPageInitonPageLoadonLoadStateonSaveStateonPostbackData, etc. They will guarantee the correct page construction.
You can override any of them, but probably you'll never need to do it except one of them.
 
public function onPageLoad() {
    if (!$this->isPostback()) {
        $this->txtName->setValue("John Smith");
    }
}
 
This method is called every time a page is loaded. It is also called when a control submits back a "post" action, like when a button is clicked, a drop
down control generates onChange event, etc. This method is never called on "ajax" action. 
For all the widgets, which need to submit back data to the Controller you can set the "SubmitMode" property. Possible values are "post", "ajax", "js" or "none".
You can set a widget's properties 2 ways. All the keys are converted to lowercase and hiphens removed, so "Text" in this case is the same as "teXt" and
"SubmitMode" is the same as "submit-mode".
 
$page->btnSubmit->render(array(
                             "Text" => "SUBMIT",
                             "SubmitMode" => "post"
                         ));
 
The second way:
 
$page->btnSubmit->setProperty("teXt", "SUBMIT");
$page->btnSubmit->setProperty("submit-mode", "post");
$page->btnSubmit->render();
 
Let's see how the events are triggered. Let's take for example the submit button. If it's "SubmitMode" is "post" or "ajax", then it's event handler will reside in
the test.php.script. If it is "js" it will reside as javascript function in test.php. And here comes a little trick, when it is "ajax" or "post" and we have defined
a javascript handler it will be called before the server handler. If the javascript handler returns "false", then the server method won't be called. This is very practical
when we want to do input validation on the client before sending data to the server.
It is very important always to place in every *.php file just after the <head> tag the following code. It will add a lot of system JavaScript code and head tags.
 
<?php $page->head->title("Test")->render(); ?>
 
It is very important always to place in every *.php file at the bottom the following code. It will complete the page.
 
<?php $page->complete(); ?>
 
Every widget which you will define in the PHP class will be registered with the JavaScript variable page of type ClientPage
JavaScript
 
page.registerWidget(new TextBox("txtName", {}));
page.registerWidget(new Button("btnSubmit", {"mode":"ajax"}));
 
// later you can use it like
var value = page.txtName.getValue();
page.txtName.setEnabled(FALSE);
 
When a button is clicked, the framework enumerates all the registered widgets and sends back to the Controller their values. This model is designed in
such a fashion, because when we have a more complicated widget like a date picker, which renders to 3 dropdown controls, it's getValue method knows very well
how to construct the final value. Also there are many free javascript widgets, which can be used just with a simple PHP wrapper around them.
There is a very well organized communication channel between the PHP variable "page" of type \web\Page and the JavaScript variable "page" of
type ClientPage. The widgets rely on the same mechanism as well.
DataGrid talks to ClientDataGrid
Tabs talks to ClientTabs
and so on.
Another possible way of communication is through the Model. There are again 2 objects of type DataModel and ClientDataModel, which exchange data.
PHP
 
$page->model->setProperty("key2", "City of London", TRUE);
$page->model->setProperty("key3", "The girl from Paris", TRUE, TRUE);
 
Then in the browser both keys would be accessible through:
JavaScript
 
var value2 = page.model.getProperty("key2");
var value3 = page.model.getProperty("key3");
 
And if later on a "post" submit or an "ajax" call is made to the server, "key3" will still be available. Take a look at the 2 TRUE parameters. They make the parameter stateful.
Also we can achieve the same thing using JavaScript:
JavaScript
 
page.model.setProperty("key3", "The girl from Paris", true);
 
The framework has been optimized for years, it is very fast exchanging data back and forth.
05. How to deal with errors. The exception handling mechanism
In your "config.inc" file you can setup these lines
config.inc
 
$app = \web\Application::create();
$app->errorHandler->set_log_to_file(TRUE);        // log all errors in protected/log/error.log
$app->errorHandler->set_log_to_screen(FALSE);     // show errors in the browser
$app->errorHandler->set_log_browser_errors(TRUE); // log all JavaScript errors in protected/log/browser.log
 
If you set the last line to TRUE then the framework will add automatically for every web page the following code, which will capture and log all JavaScript errors.
JavaScript
 
if (window.addEventListener) {
    window.addEventListener('error', function(err) {
        page.submitJsError(err);
    }, false);
}
else if (window.attachEvent) {
    window.attachEvent('onerror', function(err) {
        page.submitJsError(err);
    });
}
 
This is where possible exceptions might be thrown.
PHP
 
<?php
 
   class TestPage extends \web\Page {
 
       public function __construct() {
           $this->txtName     = new \web\widget\TextBox();
           $this->btnSubmit   = new \web\widget\Button();
       }
 
       public function onPageLoad() {
           // possible exception
       }
 
       public function btnSubmit_onClick() {
           // possible exception
       }
    }
?>
 
Internally the framework will capture them by wrapping the method call like
PHP
 
try {
    $this->onPageLoad();
    $this->btnSubmit_onClick();
}
catch (\Exception $ex) {
    $this->onPageException($ex);
}
 
The internal onPageException handler looks like
PHP
 
public function onPageException($ex) {
    if (is_class($ex, "\exceptions\ValidationException")) {
 
        if ($ex->is_show_on_screen()) {
            $this->show_error($ex->getMessage());
        }
    }
    else if (is_class($ex, "\exceptions\FrameworkException")) {
        $code = log_exception($ex);
        if ($ex->is_show_on_screen()) {
            if (app()->is_dev_mode()) {
                $this->show_error($ex->getMessage());
            }
            else {
                $errorMessage = $this->get_default_error_message();
                $errorMessage = str_replace("[code]", $code, $errorMessage);
                $this->show_error($errorMessage);
            }
        }
    }
    else if (is_class($ex, "\Exception") || is_class($ex, "\Throwable")) {
        log_exception($ex);
        $this->show_error($ex->getMessage());
    }
}
 
The handler will log the error in "protected/log/error.log" and will show a message in the browser saying an error has occured. It will log the error
with unique code and will show a general message with this code. No sensitive information will be send to the browser.
You can always override this handler to suit your needs.
06. Validate input data
When you have a form with input fields like text boxes, date fields, etc. before placing this information in the database you want to validate whether
the input data is valid. For this you will use the built-in class \util\DataValidator.
You can use it 2 ways. Either by creating instance of it or using its static methods.
The first way by using the class methods no exceptions will be thrown in case wrong data is found. The class will collect all errors in a local variable and at the end
it will be up to you what to do.
PHP
 
 // on button click
 public function btnSubmit_onClick() {
     $validator = new \util\DataValidator();
     $validator->isNotEmpty($this->tfUsername->getValue(), "Username must be specified."); // error
     $validator->isEqual($this->tfPassword->getValue(), "dW1vHy6", "Password is not valid."); // error
     $validator->isMoney(12.50, "Not a valid price"); // ok
     if ($validator->hasErrors()) {
         // do something with the errors
         // it will print both "Username must be specified." and "Both passwords must be equal."
         print_r($validator->getErrors());
     }
 }
 
The second way is to use the static methods. Then every method will raise \exceptions\ValidationException when wrong data is encountered.
PHP
 
 // on button click
 public function btnSubmit_onClick() {
     \util\DataValidator::checkEmpty($this->tfUsername->getValue(), "Username must be specified."); // exception
     \util\DataValidator::checkEqual($this->tfPassword->getValue(), "dW1vHy6", "Password is wrong."); // exception
     \util\DataValidator::checkMoney(12.50, "Not a valid price"); // raises exception
 }
 
Every instance of \web\Page has class property "validator" of type \util\DataValidator, so there is no real need to make new one.
07. Internationalization. Add a new language to your website
All locales are stored under protected/i18n/. If you list the directory "en", you'll see many files ending with ".i18n". The major one is "default.i18n". In case you want to add
a new language version, in this case the Spanish one, then you have to copy "en" to "es" and translate "default.i18n". The rest of the files belong to different widgets. Translate
them only in case your website uses these widgets.
protected/i18n/es/default.i18n
 
[page charset]
SYS_PAGE_CHARSET="utf-8"
 
[shared]
BTN_OK="Vale"
BTN_CLOSE="Cerrar"
BTN_CANCEL="Cancelar"
BTN_YES="Si"
BTN_NO="No"
STR_WHITE_HOUSE="Casa Blanca"
 
You have to save all locale files with the right encoding, which is "UTF8". The quotation marks are not obligatory, but it is always better to use them because sometimes
the special language characters can break the function which parses these files. 
Then to change to the new language version all you have to do is:
PHP
 
public function btnSpanish_onClick() {
    $this->setLocale("es");
 
    $str1 = \core\Locale::get("STR_WHITE_HOUSE");
    $str2 = i18n("STR_WHITE_HOUSE"); // alias for the above line
}
 

The default language version is English. You can always change it to whatever you'd like in config.inc
config.inc
 
$app = \web\Application::create();
$app->setName("app-myApp");
$app->setDevMode(TRUE);
$app->setDefaultLocale("es"); // change default locale to Spanish
 
08. Dealing with data sources
You use \collection\DataSource to store list of records. It's main usage is when the database "select" operation returns an object of this class and then you bind
this object to a data grid. Let's see what else you can do with it.
This is the way to manually create and fill it.
PHP
 
$dataSource = new \collection\DataSource();
$dataSource->addRecord(array(
                 "id" => 389,
                 "name" => "apple",
                 "type" => "fruit"
              ));
 
$dataSource->addRecord(array(
                 "id" => 764,
                 "name" => "rabbit",
                 "type" => "animal"
              ));
 
$dataSource->insert0(array(
                 "id" => 733,
                 "name" => "sea bass",
                 "type" => "fish"
               ));
 
This is the way to sort the internal records.
PHP
 
$dataSource = new \collection\DataSource();
$dataSource->sortBy("name", "string", SORT_ASCENDING); // The 2nd parameter can be "string" or "int".
 
// or omitting the 2nd and 3rd params as those values are the default ones
$dataSource->sortBy("name");
 
This is the way to load data from an xml file. This file must be stored under "protected/xml".
PHP
 
$dataSource = \collection\DataSource::load("books.xml");
 
protected/xml/books.xml
 
<DataSource>
 
    <record>
        <key name="id">51</key>
        <key name="title">Pride and Prejudice (Unabridged)</key>
        <key name="author">Jane Austen</key>
        <key name="pages">322</key>
        <key name="description">
            Lorem ipsum dolor sit amet, consectetur adipiscing elit.
        </key>
    </record>
 
    <record>
        <key name="id">6453</key>
        <key name="title">The Last Lecture</key>
        <key name="author">Randy Pausch and Jeffrey Zaslow</key>
        <key name="pages">255</key>
        <key name="description">
            Morbi eleifend placerat arcu. Donec scelerisque commodo mi vel sollicitudin.
        </key>
    </record>
 
</DataSource>
 
This is the way to process an existing data source. In this scenario we will add a new column, modify an existing column and we'll calculate the sum of all prices.
PHP
 
$totalAmount = 0;
for ($i = 0; $i < $dataSource->count(); $i++) {
    $record = $dataSource->getRecord($i);
    $record["price"] = $record["price"] * 1.2; // modify column
    $record["currTime"] = time(); // new column
    $dataSource->setRecord($i, $record);
 
    $totalAmount += $record["price"];
}
 
Once you have an existing data source you can manipulate it. You can keep or remove certain records.
PHP
 
$dataSource = \collection\DataSource::load("books.xml");
 
// keep all books that have 100 pages
$dataSource->keep("pages", 100);
 
// keep all books that starts with "The". Here you can use any reg expression 
// that can be used with the function "preg_match"
$dataSource->keep("title", "/^The.*/i");
 
// remove all books that have 100 pages
$dataSource->remove("pages", 100);
 
// remove all books that starts with "The".
$dataSource->remove("title", "/^The.*/i");
 
09. Interact with the database - select, insert, update, delete. Call stored procedures
First let's see how you should configure your database layer. This is done in the config.inc file. 
config.inc
 
$app = \web\Application::create();
 
$db = \database\implementation\PostgreSql::create("host=127.0.0.1 dbname=db123 user=user123 password=pass123");
$db->setAutoConnect(TRUE);
$db->setCheckForSqlInjection(TRUE);
$db->setDebugMode(TRUE);
$db->setPersistent(TRUE);
 
$app->addDatabase($db);
 
 
 
$db = \database\implementation\MySql::create("localhost", "db123", "user123", "pass123");
$db->setAutoConnect(TRUE);
 
$app->addDatabase($db, "mysql");
 
Now let's see how you should use the instantiated database object.
test.php.script
 
public function btnUseDatabase_onClick() {
    $db = \web\Application::db();
    // $db = \web\Application::db("mysql");
 
    $db->insert(...);
    $db->update(...);
    $db->delete(...);
    $db->select(...);
    $db->call(...);
}
 
$db->setAutoConnect(TRUE);
If auto connect is TRUE then you go and directly call insert, update, etc. The connection to the database is created automatically by the framework.
If the value is FALSE then you have to open and close the connection manually.
test.php.script
 
$db = \web\Application::db();
$db->connect();
$db->insert();
$db->release_connection();
 
$db->setDebugMode(TRUE);
If debug mode is TRUE then all operations like insert, update, etc. will log their sql statements in "protected/log/sql.log". For production
environments this must be always FALSE. 
$db->setPersistent(TRUE);
If persistent is TRUE then the connection to the database is done through mysql_pconnect or pg_pconnect instead of mysql_connect or pg_connect.
Basically it uses a database pool and is much faster. For more details check the PHP documentation.
SELECT :: USE DIRECT SQL
 
$db = \web\Application::db();
 
$sql = SqlStatement("select * from tbl_persons where person_id=[:id]");
$sql->bindInteger(":id", 1001);
$dataSource = $db->select($sql);
 
SELECT :: READ SQL FROM FILE
 
// place in file protected/sql/person.sql these statements
 
SQL_PERSONS_LIST=
  select p.person_id, p.first_name, p.last_name, p.birthday,
         p.country_code, p.single, c.name
  from persons p, countries c
  where p.country_code = c.country_code
        and (p.first_name = [:first] or p.last_name = [:last])
;
 
SQL_PERSONS_LIST_CRITERIA=
  select p.person_id, p.first_name, p.last_name, p.birthday,
         p.country_code, p.single, p.salary, p.last_updated, c.name as country_name
  from persons p, countries c
  where p.country_code = c.country_code [criteria]
  order by [orderString]
;
 
$db = \web\Application::db();
 
$sql = SqlStatement("SQL_PERSONS_LIST");
$sql->bindString(":first", "John");
$sql->bindString(":last", "Smith");
$dataSource = $db->select($sql);
 
INSERT
 
$db = \web\Application::db();
 
$personID = $db->sequenceNextValue("seq_default");
 
$data = \database\sql\SqlParams::create();
$data->set_integer("person_id", $personID);
$data->set_string("first_name", "Joe");
$data->set_string("last_name", "O'Connel");
$data->set_string("country_code", "uk");
$data->set_date("birthday", "1974-12-30");
$data->set_number("salary", "6500.56");
$data->set_boolean("single", TRUE);
$data->set_blob("photo", file_get_contents("jpg_photo.jpg"));
$data->set_timestamp("last_updated", "2010-01-30 23:30");
 
$db->insert("tbl_persons", $data);
 
UPDATE
 
$db = \web\Application::db();
 
$data = SqlParams(); // short way
$data->set_string("first_name", "John");
$data->set_string("last_name", "Myers");
$data->set_string("country_code", "de");
$data->set_date("birthday", "1910-11-13");
$data->set_number("salary", "9250.00");
$data->set_boolean("single", FALSE);
$data->set_blob("photo", file_get_contents("png_photo.png"));
$data->set_timestamp("last_updated", NULL);
 
$filter = SqlParams();
$filter->set_integer("person_id", 234234);
 
$affected = $db->update("tbl_persons", $data, $filter);
 
DELETE
 
$db = \web\Application::db();
 
$data = SqlParams(); // short way
$filter->set_integer("person_id", 242343);
$filter->set_date("birthday", "1910-11-13");
$filter->set_number("salary", "9250.00");
 
$affected = $db->delete("tbl_persons", $filter);
 
EXECUTE
 
$db = \web\Application::db();
 
$sql = \database\sql\SqlStatement::sql("update tbl_temp set person_lname=[:last] where person_id=[:id]");
$sql->bindString(":last", "Myers");
$sql->bindInteger(":id", 24234);
$affected = $db->execute($sql);
 
CALL STORED PROCEDURE
 
$db = \web\Application::db();
 
$params = SqlParams(); // short way
$params->set_string(0, "The result is: ");
$params->set_integer(1, 8);
$params->set_integer(2, 4);
$params->set_timestamp(3, "2003-11-18 23:30:00");
$funcReturnValue = $db->call("multiply", $params);
// The result is: 32, called on 2003-11-18 23:30:00
 
 
## MYSQL function multiply
DELIMITER $$
CREATE FUNCTION multiply(aLabel VARCHAR(32), aX INT, aY INT, aTime TIMESTAMP) RETURNS VARCHAR(64) DETERMINISTIC
BEGIN
   RETURN concat(aLabel, aX*aY, ', called on ', aTime);
END $$
DELIMITER ;
 
## POSTGRESQL function multiply
CREATE or REPLACE FUNCTION multiply(text, int4, int4, timestamp) RETURNS text AS $$
DECLARE
    aLabel             alias for $1;
    x                  alias for $2;
    y                  alias for $3;
    aTimestamp         alias for $4;
BEGIN
    return aLabel || x*y || ', called on ' || aTimestamp;
END;
$$ LANGUAGE plpgsql;
 
TRANSACTIONS
 
$db = \web\Application::db();
 
$$db->beginTransaction();
$db->insert(...);
$db->update(...);
$db->delete(...);
$db->commit();
 
10. Create complex database selects - Criteria API
The framework provides powerful and easy functionality to create queries with complex "where clause", which depends on the values of the data we pass.
Imagine a "choose your search criteria" screen with 20 fields, where the user chooses randomly only 5 of them. Building the final query could be a nightmare
using complex conditional logic. The framework provides elegant way to handle such scenarios.
Criteria API
 
$sql = \database\sql\SqlStatement::sql("SQL_ACTORS_LIST_CRITERIA");
 
$fieldPlayerId      = \database\sql\SqlCriteria::field('a.actor_id', 'number')
                            ->equals($this->ftPersonId->getValue());
 
$fieldFirstName     = \database\sql\SqlCriteria::field('a.first_name', 'string')
                            ->contains($this->ftFirstName->getValue());
 
$fieldLastName      = \database\sql\SqlCriteria::field('a.last_name', 'string')
                            ->contains($this->ftLastName->getValue());
 
$fieldCountry       = \database\sql\SqlCriteria::field('a.country_code', 'array')
                            ->in(array_keys($this->ftCountry->selectedItems()));
 
$fieldSalaryFrom    = \database\sql\SqlCriteria::field('a.salary', 'number')
                            ->greater_equal($this->ftSalaryFrom->getValue());
 
$fieldSalaryTo      = \database\sql\SqlCriteria::field('a.salary', 'number')
                            ->less_equal($this->ftSalaryTo->getValue());
 
$fieldBornAfter     = \database\sql\SqlCriteria::field('a.birthday', 'date')
                            ->greater_equal($this->ftBornAfter->getValue());
 
$fieldBornBefore    = \database\sql\SqlCriteria::field('a.birthday', 'date')
                            ->less_equal($this->ftBornBefore->getValue());
 
$fieldLUAfter       = \database\sql\SqlCriteria::field('a.last_updated', 'timestamp')
                            ->greater_equal($this->ftLastUpdatedAfter->getValue());
 
$fieldLUBefore      = \database\sql\SqlCriteria::field('a.last_updated', 'timestamp')
                            ->less_equal($this->ftLastUpdatedBefore->getValue());
 
$fieldSingle        = \database\sql\SqlCriteria::field('a.is_single', 'bool')
                            ->equals($this->ftBoolItems->getItemValue("single"));
 
$group = \database\sql\SqlCriteria::group('and');
$group->add($fieldPlayerId);
$group->add($fieldFirstName);
$group->add($fieldLastName);
$group->add($fieldCountry);
$group->add($fieldSalaryFrom)->add($fieldSalaryTo);
$group->add($fieldBornAfter)->add($fieldBornBefore);
$group->add($fieldLUAfter)->add($fieldLUBefore);
$group->add($fieldSingle);
 
$criteria = \database\sql\SqlCriteria::create();
$criteria->set_first_group($group);
// $criteria->add_group('and', $group2);
// $criteria->add_group('or', $group3);
$strCriteria = $criteria->toSql();
$sql->replace("[criteria]", $strCriteria ? "and " . $strCriteria : "");
 
$sqlString = $sql->toString();
 
It will take you some time to understand and use it. But the framework provides even more elegant solution.
 
$firstName = "John";
$lastName = "";
 
$statement = SqlStatement("
    select a.actor_id, a.first_name, a.last_name, a.birthday,
           a.country_code, a.sex, a.photo_small, c.country_name
 
      from tbl_actors a, tbl_countries c
 
     where a.country_code = c.country_code
           and lower(a.first_name) LIKE [:fname]
           and lower(a.last_name) LIKE [:lname]
");
 
if (!empty($firstName)) {
   $statement->bindString(":fname", '%' . strtolower($firstName) . '%');
}
 
if (!empty($lastName)) {
   $statement->bindString(":lname", '%' . strtolower($lastName) . '%');
}
 
$statement->compact();
 
$datagrid->bindSQL($statement);
 
After the execution of
$statement->bindString(":fname", ...);
the corresponding line will become and lower(a.first_name) LIKE '%john%',
but this code
$statement->bindString(":lname", ...);
won't be called, because the variable "$lastName" is empty string and the line and lower(a.last_name) LIKE [:lname] won't change.
When you call compact at the end it will go and remove from the SQL query all the lines which contain tokens like [:lname].
11. ORM
The framework provides powerful and simple ORM functionality. It works only with tables, which have one-to-one relationship. Imagine we have 3 tables:
tbl_employees
tbl_employees_extra_info_1
tbl_employees_extra_info_2
The last 2 tables reference the 1st one.
It will always create records in all of the 3 tables. Make sure appropriate table(s) structure has been choosen.
MySql, PostgreSQL and Oracle use different approaches for autoincrement primary key columns. MySql uses the "AUTO_INCREMENT" keyword. 
PostgreSQL uses the "IDENTITY" keyword or sequence. For Oracle a special trigger has to be created like the example above.
You can unite all the approaches by simply using a sequence by setting it with $this->set_sequence("seq_default"); in the model. In this
case you don't need autoincrement columns or triggers. Because MySQL doesn't support naturally sequences you have to create a special stored function like this:
MySQL Stored Function for sequence
 
CREATE TABLE tbl_sequences (
    name VARCHAR(32),
    last_value INT UNSIGNED NOT NULL,
    PRIMARY KEY(name),
    CONSTRAINT uq_name UNIQUE (name)
) TYPE=innodb;
 
 
DELIMITER $$
CREATE FUNCTION sequence_next_value(seqName VARCHAR(32)) RETURNS INT UNSIGNED MODIFIES SQL DATA DETERMINISTIC
BEGIN
   DECLARE affected, val, lockVal INT UNSIGNED;
 
   SELECT GET_LOCK(seqName, 10) INTO lockVal;
 
   UPDATE tbl_sequences SET last_value=last_value+1 WHERE name = seqName;
   SET affected = ROW_COUNT();
   IF affected = 0 THEN
      INSERT INTO tbl_sequences(name, last_value) VALUES(seqName, 1000);
   END IF;
   SELECT last_value INTO val FROM tbl_sequences WHERE name=seqName LIMIT 1; 
 
   SELECT RELEASE_LOCK(seqName) INTO lockVal;
 
   RETURN val;
END $$
DELIMITER ;
 
12. Data repeater
You use \util\DataRepeater to visualize the data stored in a data source. In this tutorial we will see how to create a data grid using data source and data repeater.
13. Cache, cache & cache
In your config.inc file you can set any of these options. This way you can "cache" string(s), array(s), object(s), etc.
config.inc
 
\web\Cache::setFileBased();    // it will store the data in "protected/persistent/cache/data/"
\web\Cache::setMemoryBased();  // it will store the data in memory; see APC/APCU for more info
 
How to use
 
$dataSource = \web\Cache::get("exampleDataSource");
if ($dataSource == NULL) {
    $db = \web\Application::db();
    $dataSource = $db->select(SqlStatement("SQL_ACTORS_LIST"));
    \web\Cache::set("exampleDataSource", $dataSource);
}
 
Another thing is the page-level cache.
The following code can be applied for any single page. Place at the very top of the page which you want to cache the following 2 constants:
CACHE_ENABLED and CACHE_TTL
 
<?php define("CACHE_ENABLED", TRUE); ?>
<?php define("CACHE_TTL", 24*60); ?>
 
<?php include "test.php.script"; $page = createPageInstance(); ?>
 
<html>
<head>
<?php $page->head->title("Test")->render(); ?>
</head>
 
<body style="padding-top: 30px; text-align:center;">
 
<?php $page->txtName->render(array(
                          "Style" => "width: 200px; height: 30px",
                      )); ?>
 
<br/>
<br/>
 
<?php $page->btnSubmit->render(array(
                            "Text" => "Submit",
                            "Class" => "cssBlueDemoButton",
                            "Style" => "width: 200px",
                            "SubmitMode" => "ajax"
                        )); ?>
 
</body>
 
</html>
 
<?php $page->complete(); ?>
 
That's it, the 2nd constant is optional. 24*60 means 1 day. In case you don't set it, the framework will use 1 day as default value. It means after 1 day the page
will expire from the cache and will be re-generated once again.
Another place to setup the page-level cache is the config.inc file. By placing there your code you can manage all pages on global level.
config.inc
 
if (!defined('CACHE_ENABLED')) define("CACHE_ENABLED", TRUE);
if (!defined('CACHE_TTL')) define("CACHE_TTL", 24*60); // 1 day
 
define("CACHE_INCLUDE_LIST", serialize(array(
       "#^http://.*/examples/.*$#i",                   // a pattern which will be validated through "preg_match"
       "#^http://.*/tutorials/.*$#i"
)));
 
define("CACHE_EXCLUDE_LIST", serialize(array(
       "#^http://.*/members/.*$#i",
       "#^http://.*/admin/.*$#i",
       "#^https://admin.mysite.com/.*$#i"
)));
 
If you don't set CACHE_INCLUDE_LIST and CACHE_EXCLUDE_LIST then every single page will be cached. If you set only CACHE_INCLUDE_LIST then only the pages which match
it's list will be cached. Imagine a vistor is trying to load in his/her browser
http://www.mysite.com/examples/index.php
then the url will match the pattern ^http://.*/examples/.*$#i and the page will be cached.
The same logic is applied for the exclusion list as well. First the framework will process CACHE_INCLUDE_LIST and if a match is found will stop
and cache the page. After that it will process CACHE_EXCLUDE_LIST and if a match is found will stop and mark the page not eligible for the cache.
HOW IT WORKS
When a page is eligible for the cach the framework saves it under "protected/persistent/cache/html/". For every page it saves 2 files:
xxx.info
xxx.cache
The first one contains the key(url) and the 2nd the content. You can go and delete them any time you want. 
If a page is eligible then it won't be saved there if: 
- $_POST contains any parameter 
- it is an Ajax request 
When creating the key for the url the framework checks for 2 more things: 
- current locale(if the current language is "en", "fr", "de", etc.) 
- if the site visitor is logged-in. 
It is very important, when the developer authenticates the visitor to set a unique ID in the session through $page->user->setId("123456"); 
At the end for a visitor who used the french locale and is logged-in the key will look like: 
(fr,123456)http://www.mysite.com/demo/index.php 
and the framework will start caching the pages only for him/her. This may result in a 1000s of pages. In general it is not a good idea
to cache such kind of pages.
14. How to login users
In your config.inc file you configure the member zones like this:
config.inc
 
$app = \web\Application::create();
$domainZone = $app->createDomainZone("mysite.com");
 
$memberZone = $domainZone->createMemberZone("MemberToken");
$memberZone->setLoginPage("/members/login.php");
$memberZone->addProtectedUri("#^.*/members/.*$#i", TRUE);  // line #5
$memberZone->enforceSSL();
 
$adminZone = $domainZone->createMemberZone("AdminToken");
$adminZone->setLoginPage("/admin/login.php");
$adminZone->addProtectedUri("#^.*/admin/.*$#i");
 
The addProtectedUri accepts as first argument a regular expression pattern, the second argument is optional and means whether the client
will be enforced to use https connection. Let's see how it works.
1. A client enters http://www.mysite.com/members/ in his browser
2. The framework sees that he is not logged-in and the URL matches against line #5 and redirects him to https://www.mysite.com/members/login.php
3. He types his username and password and if they are valid the system logs him as user with token "MemberToken".
login.php.script
 
public function onPageLoad() {
    $this->captureInitialUri();
}
 
public function btnLogin_onClick() {
    $this->validator->setPolicyRaiseError(VALIDATOR_POLICY_RAISE_ERROR_PER_WIDGET);
 
    $this->txtUsername->checkEmpty("Username is empty");
    $this->txtPassword->checkEmpty("Password is empty");
 
    $db = \web\Application::db();
 
    $sql = SqlStatement("select * from users where username = [:user] and password = [:pass]");
    $sql->bindString(":user", $this->txtUsername->getValue());
    $sql->bindString(":pass", $this->txtPassword->getValue());
    $dataSource = $db->select($sql);
 
    if ($dataSource->isEmpty()) {
        throw new \exceptions\ValidationException("Your Username and Password are not valid.");
    }
 
    $this->user->setLoggedAs("MemberToken");
    $this->user->setId($dataSource->record(0)["id"]);
    $this->user->setUsername($dataSource->record(0)["username"]);
    $this->user->addRole("view");
    $this->user->addRole("create");
    $this->user->addRole("delete");
 
    $this->redirect($this->initialUriOr("/default_page.php"));
}
 
If you are not logged-in and you load in the browser:
https://www.mysite.com/members/member.php?ID=823742364
the system will redirect you to the login page and then there is an option to call captureInitialUri to save the above URL in the model.
And once the user's credentials are validated then he can be redirected directly to the initial URL.
Later on hasRole can be used to restrict the user based on his permissions.
catalog.php.script
 
public function btnDeleteProduct_onClick() {
 
    if (!$this->user->hasRole("delete")) {
        throw new \exceptions\SecurityException("You do not have permission to delete products.");
    }
 
}
 
15. Tips, tricks and best practices
Let's say you have the pages test.php and test.php.script
If you define in one of these files the function output_buffer_post_process_current_page, then you are able to process the final HTML.
PHP
 
function output_buffer_post_process_current_page($html) {
    $html = str_replace("XXX", "YYY", $html);
    return $html;
}
 
For a global scale page processing you can define the function output_buffer_post_process in the config.inc file.
On the JavaScript side the framework uses a small jQuery-like functionality to deal with the HTML DOM model. It is very simple and efficient, but
not as powerful as the before mentioned library.
JavaScript :: Handle HTML DOM elements using elementX('ID')
 
// There is a mini jquery class, which is used by the framework to manipulate the HTML elements' attributes.
 
function btnTest_onClick() {
    var x = elementX("ID");
 
    var content = x.html(); // for DIV, SPAN, P, TD, etc.
    x.html("Text 123");
 
    var w = x.width(); // for DIV, SPAN, P, TD, etc.
    x.width(300);
 
    etc.
}
 
// for more see the pdk-dom.js
 
There are a couple of PHP encrypt/decrypt global functions. If no password is provided, then the default framework password will be used.
It is stored in "system/classes/Constants.inc" under the name "SYS_INTERNAL_ENCRYPT_PASSWORD". It is advisable to change it for
every application you create. The default encryption method is XXTEA. The encrypted string is also "base64" encoded.
PHP :: Encrypt and Decrypt
 
public function btnSubmit_onClick(){
    $str = encryptString("My sensitive text");
    $str = encryptString("My sensitive text", "pass1");
    $str = encryptArray(array());
    $str = encryptArray(array(), "pass1");
    ...
    $str2 = decryptString($str);
    $str2 = decryptString($str, "pass1");
    $arr = decryptArray($str);
    $arr = decryptArray($str, "pass1");
}
 
// using the security classes
public function btnSubmit_onClick(){
    $secretKey = "pass1";
 
    $security = \security\SecurityFactory::createInstance("XXTEA", $secretKey); // or
    $security = \security\SecurityFactory::createInstance("MCRYPT", $secretKey);
 
    $str = $security->encrypt("My sensitive text");
    ...
    $str2 = $security->decrypt($str);
}
 
Sometimes you may want to do something based on the browser type.
JavaScript
 
function btnSubmit_onClick(){
    if (page.agent.isInternetExplorer()) {
        // code for IE
    }
    else if (page.agent.isFirefox()) {
        // code for Firefox
    }
    else {
        // handle the rest
    }
}
 
PHP
 
public function btnSubmit_onClick(){
    if ($this->agent->isInternetExplorer()) {
        // code for IE
    }
    else if ($this->agent->isFirefox()) {
        // code for Firefox
    }
    else {
        // handle the rest
    }
}
 
Redirect the visitor to "https://" if "http://"" has been detected.
PHP
 
public function onPageLoad() {
    if (!$this->isPostBack() && $this->request->isHTTPS()) {
        return $this->loadHTTPS();
    }
}
 
There is a PHP global function called "randomString", which produces random strings with variable length consisting
only of A-Z, a-z, 0-9 characters.
PHP
 
public function onPageLoad() {
    $str1 = randomString(5); // f9uR7
    $str1 = randomString(7); // sS1r5Ty
}
 
There is a PHP global function called "filesizeToString", which converts bytes length to human readable text.
PHP
 
public function onPageLoad() {
    $str1 = filesizeToString(8*1024*1024 + 20*1024); // 8mb 20kb
}
 
If you have installed the extension "GD", which provides functions for manipulating images then you can create a thumbnail in 1 line:
PHP
 
public function btnSave_onClick() {
    $binaryThumbImage = \web\widget\Image::createThumbnail($binaryData, 32, 32);
}
 
On image upload you can check the dimension like this:
PHP
 
public function btnSave_onClick() {
    $dim = \web\widget\Image::getDimensionFromBinaryData($file->getBinaryData());
    if ($dim['width'] > 700 || $dim['height'] > 500) {
        return $this->showError("Image is too big");
    }
}
 
16. Benchmark
If installed on any modern machine like CPU Intel i7 last generation, 32GB RAM and SSD hard drive the framework is extremely fast. Another essential requirement
which greatly improves it's speed is PHP code cache like "opcode". If you run a website in shared hosting environment where the "opcode" cache is usually
disabled then it slows down, but it applies for any other framework. As PHP DEK supports various levels of cache workarounds can partly solve this inconvinience.