Developing a Webasyst app

Tutorials » #1. A step-by-step tutorial on how to develop a Guestbook app using Webasyst framework

A step-by-step tutorial on writing a “Guestbook” app using Webasyst framework.

1. Debug mode

Before you begin the development, ensure that you have enabled debug_mode (this mode disables any caching usually performed by the framework). You can enable the debug mode either in the settings of the Installer app or in file wa-config/config.php:

'debug' => true

2. Creating application file structure

First of all you will need installed Webasyst framework on your computer or a remote web server. Download and install it as described in the installation manual.

Let the application's unique identifier (APP_ID) be guestbook and create a subdirectory with the same name inside framework directory wa-apps/, i.e. wa-apps/guestbook/. This will be the main storage for all application files: PHP files, HTML templates, CSS styles, JavaScript, etc. It is essential that the name of the newly created subdirectory must match the application identifier.

The backend (administrative interface) of the application will display the list of all messages posted in the guestbook in the chronological order with the ability for a user to delete each record. The frontend (publicly accessible interface) will also display all previously posted messages and a web form to post a new message.

Create several subdirectories inside wa-apps/guestbook/ by following the recommendations on creating the file structure for Webasyst applications:

wa-apps/guestbook/img/: image files
wa-apps/guestbook/lib/: PHP scripts (configuration files and modules)
wa-apps/guestbook/lib/config/: configuration
wa-apps/guestbook/templates/: HTML templates (Webasyst utilizes Smarty template engine by default)
wa-apps/guestbook/locale/: application localization files (Webasyst utilizes gettext as the localization management tool by default)

3. Describing the application

Create the main application description file : wa-apps/guestbook/lib/config/app.php (the file name must be exactly as shown in the example). Add the following code to the file:

<?php

return array(
  'name' => 'Guestbook', // application name
  'img'  => 'img/guestbook.svg' // relative path to the application icon file
);

Because various Webasyst apps may have very different background color, we recommend using an SVG file with semitransparent background to ensure that the icon appears equally well anywhere. The icon file must be uploaded to the location specified in the configuration file: wa-apps/guestbook/img/guestbook.svg.

If you need to use bitmap images, then it is better to create several files with 16, 24, 48, and 96 pixels size, and specify relative paths to them in 'icon' item as an array:

'icon' => array(
  '16' => 'img/myapp16.png',
  '24' => 'img/myapp24.png',
  '48' => 'img/myapp48.png',
  '96' => 'img/myapp96.png',
);

Application name and the path to the icon name are the required parameters of the application description file along with its APP_ID. The description file can also contain other parameters (read more about the application configuration).

At this point everything is ready for the application icon and name to appear in the main backend menu. To make it happen, enable the application in the system configuration file  wa-config/apps.php by adding the following line to it:

'guestbook' => true

Save the file and open/refresh the backend page in your browser. The application backend is now available at http://{webasyst_installation_URL}/webasyst/guestbook/. If you have full administrative access (e.g., are logged in as the main user), you will see the new application's icon in the Webasyst main menu. The application can already by started, though it is not able to perform any useful actions yet.

4. Hello world!

Now we will create a simple web page so that the application can display some content in a browser; e.g., the classical "Hello world!".

To achieve that, we will write an action. An action is a basic structural element of any operation performed by an application. Examples of such operations: display a certain backend page, display frontend screen, save registration form data, delete a record from the database, fetch a new portion of data using AJAX, etc. An action constitutes a fragment of PHP code written as separate class method or an entire class. The framework executes a certain action depending on the input data (the contents of a GET or POST request). The choice of a specific action is made by a controller.

Logically combined controllers and actions are called modules. For example, an order processing module in some application may have the following collection of actions: "display all orders in a list", "delete an order", "add an order", "update customer information", "change order status", etc. Modules are the structural "bricks", with which an application is built; their source code resides in directory wa-apps/{APP_ID}/lib/actions/.

Each module is either 1) a separate PHP file named according to rule {APP_ID}{MODULE}.actions.php, which contains the code of a PHP class named according to rule {APP_ID}{MODULE}Actions, which extends class waViewActions and contains individual actions (methods), or 2) a subdirectory with several PHP files containing the actions execution logic.

In this example we will use the first option, i.e. module implementation as a separate PHP class with several methods representing individual actions. With this approach, class methods (actions) must be named according to rule {action}Action. A more detailed description of both code organizing methods is available in the article about the actions and controllers declaration rules.

Below is described the general working scheme of a Webasyst application:

  1. System class FrontController locates the appropriate module in accordance with the input data received from a GET or POST request. Module name is usually contained in the user request as a parameter; e.g., /?module=orders&action=add (in the backend), or according to the request routing rules (in the frontend). If no module name is explicitly specified, then the framework automatically attempts to find module backend (for the backend) or module frontend (for the frontend).
  2. Module controller determines (using the same input data) which action must be executed and transfers control to that action. If no action name is contained in a user request, than the default action is executed.

Complex applications usually consist of several modules, but for a simple guestbook we will write only one module — default module backend. First of all, we will define two actions in the module: default ("show all records") and delete ("delete a message"). Following the rules explained above, create file wa-apps/guestbook/lib/actions/guestbookBackend.actions.php and define class guestbookBackendActions in it:

<?php

class guestbookBackendActions extends waViewActions
{
  public function defaultAction()
  {
    // implementation of the message list displaying action (default action in this example)
  }

  public function deleteAction()
  {
    //implementation of the message deleting action
  }
}

We will write some code in these two methods later in step 6, when the guestbook database structure is created.

Template

Create a simple HTML template for the application backend, i.e. for action default. The template will display message "Hello world!" in a browser.

Template files for application actions must reside in directory wa-apps/{APP_ID}/templates/actions/{MODULE}/ and be named according to rule {MODULE}{ACTION}.html. The HTML file with such a name will be automatically processed after execution of the corresponding action. For our example we need to create file wa-apps/guestbook/templates/actions/backend/BackendDefault.html:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>{$wa->appName()} - {$wa->accountName()}</title>
  {$wa->css()}
  <script src="{$wa->url()}wa-content/js/jquery/jquery-1.4.2.min.js"
  type="text/javascript"></script>
</head>
<body>
  <div id="wa">
    {$wa->header()}
    <div id="wa-app">
      <div class="block">Hello world!</div>
    </div>
  </div>
</body>
</html>

This template will generate the backend HTML code structure, which can be used as a starting point in designing the backend for most Webasyst applications. In all templates you can use helper $wa with the following methods:

$wa->appName() returns the application name;
$wa->accountName() returns the account name usually displayed in the top-right corner;
$wa->css() adds HTML code to include common Webasyst style sheet (read more about designing Webasyst interface);
$wa->js() adds HTML code to include JavaScript files necessary for Webasyst functioning;
$wa->url() returns the Webasyst installation URL;
$wa->header() adds HTML code displaying the main menu with the list of installed applications.

Refresh the browser tab/window with the opened Guestbook application. Now you can see message "Hello world!" as shown below:

5. Database

So far we have created only a "dummy" application which does not perform any actions. Let us add simple business logic to the guestbook.

For storing visitor messages, create a table in a MySQL database. To do so, create file wa-apps/guestbook/lib/config/db.php and add the following code to it:

<?php
return array(
    'guestbook' => array(
        'id' => array('int', 11, 'null' => 0, 'autoincrement' => 1),
        'name' => array('varchar', 255, 'null' => 0),
        'text' => array('text', 'null' => 0),
        'datetime' => array('datetime', 'null' => 0),
        ':keys' => array(
            'PRIMARY' => 'id',
            'datetime' => 'datetime',
        ),
    ),
);

This description is required for installation of the application (e.g., on another server or domain name) and for its uninstallation in order to automatically delete unnecessary database tables. The names of database tables must begin with the application's APP_ID (in our example it is guestbook) to avoid naming conflicts with other applications' tables.

Define the database manipulation model by creating file wa-apps/guestbook/lib/models/guestbook.model.php with the following contents:

<?php

class guestbookModel extends waModel
{
  protected $table = 'guestbook'; // table name
}

This code will allow the application to work with the data contained in table guestbook using Webasyst framework model class to access the database (read more about the database model).

6. Frontend

Storage space for guestbook messages is ready, so, except for the implementation of backend actions default and delete, we only have to add a frontend where visitors will be able to post their messages.

Frontend controller

A frontend is created according to the same rules as the backend action displaying message "Hello world!". Create frontend controller file wa-apps/guestbook/lib/actions/guestbookFrontend.action.php.

The guestbook frontend will contain only one page with a list of all posted messages and will process POST requests adding new messages to the database. Therefore, instead of defining method defaultAction, we will take an easier way by declaring a controller class extending system class waViewAction (instead of waViewActions) and will define method execute in it to implement the entire frontend logic. This approach is suitable for modules containing only action. Below is the contents of the frontend controller file guestbookFrontend.action.php:

<?php

class guestbookFrontendAction extends waViewAction
{
  public function execute()
  {
    // creating a model instance to retrieve data from the database
    $model = new guestbookModel();
    // if a POST request is received, then add a new record to the database
    if (waRequest::method() == 'post') {
      // extract data from the POST request
      $name = waRequest::post('name');
      $text = waRequest::post('text');
      if ($name && $text) {
        // add a new record to the database table
        $model->insert(array(
          'name' => $name,
          'text' => $text,
          'datetime' => date('Y-m-d H:i:s')
        ));
      }
    }
    // retrieve guestbook records from the database
    $records = $model->order('datetime DESC')->fetchAll();
    // pass data on to the template file
    $this->view->assign('records', $records);
  }
}

Frontend template

Create an HTML template (using Smarty syntax) for the single available frontend action in file wa-apps/guestbook/templates/actions/frontend/Frontend.html:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>[`Guestbook`]</title>
  <style>
    .post h3 { margin-bottom: 0; }
    .post span { color: gray; font-size: 0.9em; }
    .post p { margin-top: 0; }
  </style>
</head>
<body>
  <h1>[`Guestbook`]</h1>
  <form method="post" action="">
    [`Name`]:<br />
    <input name="name" type="text"><br />
    [`Message`]:<br />
    <textarea rows="4" cols="70" name="text"></textarea><br />
    <input type="submit" value="[`Post`]">
  </form>
  {foreach from=$records item=r}
    <div class="post">
      <h3>{$r.name|escape}</h3>
      <span>{$r.datetime|wa_datetime}</span>
      <p>{$r.text|escape|nl2br}</p>
    </div>
  {/foreach}
</body>
</html>

Note that all text strings are enclosed in structure [` `]. This syntax is used for automatic translation of interface strings to the current user language and is recognized by default in all backend and frontend template files (see more details in step 7 below). If you are planning only interface language for your application, then it is not necessary to use the [` `] syntax.

Routing setup

In order to "tell" the framework, that your application has a frontend, add the following line to application configuration file wa-apps/guestbook/lib/config/app.php:

'frontend' => true

Furthermore, the frontend must be associated with a certain URL. To do so, add the following rule to the system-wide routing configuration file wa-config/routing.php:

'guestbook' => array(
  'url' => '*', // mask for the URLs at which the guestbook will be available
                   // you can also specify value guestbook/*
  'app' => 'guestbook',
  'module' => 'frontend'
)

If you have no such file, create it with the following contents:

<?php

return array(
  // routing for all hosts
  'default' => array(
    'guestbook' => array(
    'url' => '*',
    'app' => 'guestbook',
    'module' => 'frontend'
    )
  )
);

Rule 'url' => '*' means that the guestbook will be accessible via the main Webasyst URL: by typing a URL of the form http://{webasyst_installation_URL}/, you will open the guestbook frontend. If, instead of '*', you write 'guestbook/*', then the guestbook will appear at a URL of the form http://{webasyst_installation_URL }/guestbook/.

7. Backend

Let us return to actions default and delete defined earlier in step 3.

Access rights setup

To demonstrate how easily detailed access rights setup can be implemented for the backend, we will illustrate it by the example of the message deletion function in the guestbook. Let us inform the system that our application supports detailed access rights setup (in addition to standard values "access granted" and "access denied"). To do so, add the following line to application configuration file wa-apps/guestbook/lib/config/app.php:

rights => true

Then describe the access rights structure in a separate file wa-apps/guestbook/lib/config/guestbookRightConfig.class.php (note the correct file naming pattern):

<?php

class guestbookRightConfig extends waRightConfig
{
  public function init()
  {
    // declare access rights objects
    $this->addItem('delete', 'Can delete posts', 'checkbox');
  }
}

The above code sets the following values:

'delete': access rights object key (action name)
'Can delete posts': name of action displayed on the user access rights setup page (in Contacts application)
'checkbox': type of the HTML element which will be used for changing access rights status

Once the above code has been added, you can read the actual access rights value for the specific user in application actions using syntax $this->getRights('delete') as shown below in subsection Backend controller.

Backend controller

Open again controller file wa-apps/guestbook/lib/actions/guestbookBackend.actions.php and add implementation of the previously defined actions default and delete:

<?php

class guestbookBackendActions extends waViewActions
{
  /**
  * Default action, output of all guestbook messages
  * URL: guestbook/
  */
  public function defaultAction()
  {
    // create a database model instance to retrieve data from the DB
    $model = new guestbookModel();
    // retrieve guestbook messages from the DB
    $records = $model->order('datetime DESC')->fetchAll();
    // pass retrieved messages on to the template file
    $this->view->assign('records', $records);
    // pass the frontend URL on to the template file
    $this->view->assign('url', wa()->getRouting()->getUrl('guestbook'));
    /*
    * pass on the current user's access rights value for deleting guestbook messages
    * access rights objects are described in file lib/config/guestbookRightConfig.class.php
    */
    $this->view->assign('rights_delete', $this->getRights('delete'));
  }
  /**
  * deleting a message from the guestbook
  * URL: guestbook/?action=delete&id=$id
  */
  public function deleteAction()
  {
    // if a user has access right to the message deletion function
    if ($this->getRights('delete')) {
      // get the id of the message to be deleted
      $id = waRequest::get('id', 'int');
      if ($id) {
        // delete a table record
        $model = new guestbookModel();
        $model->deleteById($id);
      }
    }
    // redirect user to the main application page
    $this->redirect(wa()->getAppUrl());
  }
}

Modify backend HTML template file wa-apps/guestbook/templates/actions/backend/BackendDefault.html by replacing the line displaying message "Hello world!" with the code which will output data passed on to the template:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
  <title>{$wa->appName()} - {$wa->accountName()}</title>
  {$wa->css()}
  <script src="{$wa_url}wa-content/js/jquery/jquery-1.4.2.min.js"
  type="text/javascript"></script>
</head>
<body>
  <div id="wa">
    {$wa->header()}
    <div id="wa-app">
      <div class="block">
        <ul class="zebra">
        {foreach from=$records item=r}
          <li>
          {if $rights_delete}<a class="count" href="?action=delete&id={$r.id}">
            <i class="icon16 remove-red"></i></a>{/if}
            <span class="hint">
              <strong>{$r.name|escape}</strong>
              {$r.datetime|wa_datetime}
            </span>
            {$r.text|escape|nl2br}
          </li>
        {/foreach}
        </ul>
        <a href="{$url}" target="_blank">[`Guestbook on site`]</a>
        <i class="icon10 new-window"></i>
      </div>
    </div>
  </div>
</body>
</html>

Now the backend is functioning, too: all guestbook messages are displayed and you can delete them (if you have appropriate access rights).

8. Localization

Webasyst utilizes gettext as the localization handling mechanism; if gettext extension is not installed on the server, its PHP implementation built in the framework is used (read more about the framework framework localization).

In the HTML code of templates provided in the text of this tutorial interface localization strings are written in the English language. Strings specified inside structure [` `] are so-called localization keys. In order to translate the guestbook interface to another language (e.g., Russian), create file wa-apps/guestbook/locale/ru_RU/LC_MESSAGES/guestbook.po and compile it using cross-platform program POedit which supports gettext localization files.

Using POedit add pairs "key–value" in file locale/ru_RU/LC_MESSAGES/guestbook.po, where the key is the corresponding string used in an HTML template file and the value is its translation (into Russian in this example). Webasyst will automatically display the required translation if there is a corresponding localization file.

Below is provided a sample localization file for translating Guestbook application into the Russian language wa-apps/guestbook/locale/ru_RU/LC_MESSAGES/guestbook.po:

msgid ""
msgstr ""
"Project-Id-Version: Guestbook\n"
"POT-Creation-Date: \n"
"PO-Revision-Date: 2011-04-27 10:30+0300\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=((((n%10)==1)&&((n%100)!=11))?(0):(((((n%10)>=2)&&((n%10)<=4))&&
(((n%100)<10)||((n%100)>=20)))?(1):2));\n"
"X-Poedit-Language: Russian\n"
"X-Poedit-Country: Russian Federation\n"
"X-Poedit-SourceCharset: utf-8\n"
"X-Poedit-Basepath: .\n"
"X-Poedit-SearchPath-0: .\n"
"X-Poedit-SearchPath-1: .\n"

msgid "Guestbook"
msgstr "Гостевая книга"

msgid "Guestbook on site"
msgstr "Гостевая книга на сайте"

msgid "Name"
msgstr "Имя"

msgid "Message"
msgstr "Сообщение"

msgid "Post"
msgstr "Написать"

msgid "Can delete posts"
msgstr "Может удалять записи"

If you have installed the framework on a Windows machine with Apache used as the web server, then restarting Apache may be necessary after compilation of the localization file in order for the new language to become available for your application.

Ready-to-use Guestbook app written using instructions contained in this tutorial is available in framework's GitHub repository.