Home / Section index
 www.icosaedro.it 

 PHPLint - Web tools - bt_ web by functions

Last updated: 2018-04-16

This tutorial explains the basics of the bt_ (bee tee underscore) tool, that implements the "web by functions" programming paradigm. Basically, with bt_ each web page is generated by a specific function of the web program; on each page, user actions may trigger the invocation of another function that may display another page, and so on. Each web page, being implemented by a function, not only can take arguments, but it may also return values to the invoker function just like functions of an ordinary program; in that way, complex articulated navigation paths among pages can be established and enforced. Data are stored store server-side, nothing is exposed into the user's page; no hidden fields nor URL parameters are necessary anymore.

Contents

References

Origins

Bt_ originates from the need to solve a problem that might be stated like this: is it possible to associate functions to web pages and then handle the invocations among them just like we normally do with functions in a programming language? and is it possible passing arbitrary parameters between these functions and retrieve values as well in a simple, safe and secure way? The reply is certainly yes, and this is just the main purpose of bt_. Just a note about the name: "bt" means "back-tracking" as the original goal was to build reusable pages that may return values to their caller page whatever it might be; the result was a library of PHP functions with "bt_" prefix on their name; those functions are now replaced by a pair of classes, UserSession and WindowSession, but the concepts remains just the same.

Go-to between pages: the bt file

So, what do bt_ provides to us? Basically only two functions: a function to create a button and a function to create an anchor. These two functions generate the corresponding control element in the page bound to some specific handler function. Their usage is very simple:

<?php

require_once __DIR__ . "/../phplint/stdlib/all.php";

use it\icosaedro\web\bt_\UserSession;

class MyAppl {

	static function page1(){
		echo "<html>...</html>";
	}

	static function page2(){
		echo "<html>...</html>";
	}

	static function dashboard(){
		echo "<html><body>";
		UserSession::formOpen();
		UserSession::button("Go to page 1", "MyAppl::page1", 1, 2, 3);
		UserSession::anchor("Go to page 2", "MyAppl::page2", 4, 5, 6);
		UserSession::formClose();
		echo "</body></html>";
	}

}
Basic idea of what bt_ does.

As usual in these samples of code, I always load the "all.php" module which in turn loads the class autoloader, the error handler and the cast magic functions, all tools that are highly recommended for safe programming and PHPLint validation. The UserSession class is the main class implementing bt_, the other being WindowSession. We will explain later what a user session and a window session are. By now the UserSession class alone already provides all the features we need to use bt_ through simple static functions.

After reading the tutorial about sticky forms, you may easily guess what all these functions do: formOpen() and formClose() create the form element; the button() and anchor() functions put a button and an anchor into the page and specify the handler method to invoke along with its arguments. The only difference here is that handlers must be functions or static method of some class. For the record, these handler function invocations are named forward calls to distinguish them from the backward calls we will see next.

Just few words about the implementation. UserSession creates a session directory for each logged-in user. Under this user session directory a window session directory is created for each window the user opens in its browser, so allowing the user to compare pages or performing different tasks at once, or passing temporarily to another task without leaving the main one.

Under each window session of the user session, two files are created: the bt file and the bt stack. The bt file lists all the forward calls related to the page the user currently see. For example, the chunk of code above would write two entries in the bt file containing the name of the function and its parameters. Entries in the bt file get a number "i", and it's just that number alone that goes into the html page; not the function name, not its arguments, but just a number. When the user clicks, that number comes back and bt_ can safely retrieve from its bt file exactly which function with which arguments has to be invoked. The remote client cannot tamper with these function names nor their argument simply because these data never leave the server. No data is saved client-side apart from the session cookie and the "i" numbers.

Go-sub between pages: the bt stack

As stated above, the bt stack is a file under the window session directory where backward calls are stored. Backward calls are just like forward calls, the only difference is that backward calls are appended to the stack rather than being inserted and numbered into the bt file. Programs may invoke explicitly the top of the bt stack, so returning to some previous function. But explaining how and when backward calls are appended to the bt stack is a bit more tricky.

Lets take for example two pages implemented as two functions a() and b(), or even two static methods of some class, say A::a() and B::b(). I will use simple functions here for their shorter name. We then have a() and b(). Say that a() contains a link that triggers the invocation of b(); the user just click on this link then invoking page b(), but then for some reason according to its internal logic, b() wants to return to its caller, in this case a(). How can this be done with bt_? Here is the chunk of code that performs this task:

function a(){
	...
	// Gosub to b():
	UserSession::setCallBackward("a");
	UserSession::button("Go to page B", "b");
	...
}

function b(){
	...
	// Return (to a(), in this example):
	UserSession::invokeCallBackward();
	...
}
Performing "gosub" and "return" between functions generating web pages.

Note that a() creates a button that invokes b() using the button() function as we already seen above. But before doing that, a return point to a() itself is established with setCallBackward("a"); this function adds a backward call to a() in the bt file associated to the next forward call generated by button(); both entries of the bt file take the same number but are clearly marked being a backward call the first and a forward call the latter:

bt file:                                bt stack:
--------------------------              -----------------
i  type  func  args                     func  args
-  ----  ----  -----------              ----  -----------
1  back  a     none
1  forw  b     none
Contents of the bt file (left) with the forward and backward calls added once page a() served. The bt stack (right) is by now empty.
bt file:                                bt stack:
--------------------------              -----------------
i  type  func  args                     func  args
-  ----  ----  -----------              ----  -----------
1  ....  ....  .......                  a     none
1  ....  ....  .......
Contents of the bt file (left) once page b() served. The bt stack (right) now contains the return to a().

Now, if the user clicks on the "Go to page B" button, here it is what happens:

  1. bt_ identifies the user's session and parses the "i=1" parameter from the request.
  2. bt_ recovers both entries i=1 from the bt file associated to the window session.
  3. bt_ appends the backward call "a" to the bt stack.
  4. bt_ invokes the forward call "b" that generates the next page.
  5. After some user's interaction with page b(), the b() function decides its task is over and it's time to go back to the original caller, so it invokes the backward call from the bt stack with invokeCallBackward(): funtion a() runs again.

Note that the b() function, in order to go back to its invoker, does not have to know who the originating page really was, it just returns back to the caller whatever it is, just like re-usable functions in a programming language.

You may also imagine at go-to and go-sub between web pages as big arrows joining pages together: forward calls bring from a link triggered by the user to another page, while backward calls bring back to some other function. What distinguishes a go-sub from a go-to is the setCallBackward() function that prepares a return point.

But it's not ended here. Both setCallBackward() and invokeCallBackward() accept further arguments. By introducing arguments into setCallBackward(), the caller is saving data he wants to get back on return. And by adding arguments to the invokeCallBackward() further arguments will be added to those already existing in the backward call on the stack. So, the original function, a() in our example, may set arguments that carry its internal state, and may also receive from the callee further arguments as the result of its work.

Using the stack to save the state of a function

As explained above, a function can set a return point, possibly to itself and possibly with arguments, before setting a forward call associated to a link. In this way the function is preparing a return point AND also saves some state information he wants to get back on return from the other page. The following example should better illustrate this concept.

Say we have the function guyMask($record_id) that displays the specified record number, and say this function had been invoked with argument 1234: now the user is seeing the profile mask of some guy number 1234. Lets suppose this mask has the button "Add Friend". This button brings to some other mask that allows to search and select another record; it might be a complex task involving several masks, but at the end the user will find out that friend and will select a record, say record=5678. The task of the search mask is now over and a result can be retrieved to the original invoker. Here is how the code that performs all that might look like:

/**
 * @param int $record_id Guy to display.
 * @param int $friend_id Friend to add if not zero.
 */
function guyMask($record_id, $friend_id = 0){

	if( $friend_id != 0 ){
		// $friend_id has to be added
	}
	...
	UserSession::setCallBackward("guyMask", $record_id);
	UserSession::button("Add Friend", "searchMask");
	...
}

function searchMask(){
	...
	UserSession::invokeCallBackward($found_id);
}
Save state and retrieve values from other pages.

Here is what happens:

  1. guyMask(1234) is invoked somewhere and the user is now seeing the guy mask page.
  2. The user clicks to add a friend to the list: a backward call guyMask(1234) is appended to the stack and searchMask() is invoked.
  3. After a more or less complex interaction, the user finally finds the friend record_id=5678 he was looking for and clicks on it. This triggers the invocation of some handler method that performs a UserSession::invokeCallBackward(5678).
  4. The backward call on the top of the bt stack is retrieved, the new argument is added, and the resulting function guyMask(1234, 5678) is invoked.
  5. The guyMask() function resumes, recovers the $record_id=1234 it was originally displaying, and finally detects there is a further argument $friend_id=5678, the new friend to add: bingo!

In this simple example note how function guyMask() saves and records its state between pages invocation. Here we have a simple int, but any number of arguments of any type can be added, including arrays and objects; the only requirement is that these data be serializable (resources are not, for example). Then we may save the content of the mask before temporarily leaving for another sub-mask, and recover the mask contents later when the function is invoked back; as we will see this task comes easier using bt forms, the argument of the next article.

All this might sound too complicated at first, but it is not on practical applications. Instead, it is very easy to build navigation paths among pages, re-use pages in different contexts, exchange data and keep the state of each page among requests. Web programming as it is commonly intended today is like a programming language lacking a safe way to perform simple basic tasks as go-to and go-sub, forcing programmers to complicated, mostly unsafe and inefficient workarounds. You may want to read back again and take your time to realize what it's happening here and which are all the possible usages and implications of all this.

Putting all together: file system layout

Lets make a concrete example of the files system layout of a typical bt_ installation. I'm currently using the WAMP server system (www.wampserver.com) that provides just all I need under Windows: the Apache web server, PHP 5 and 7, PHPMyAdmin and a nice integrated controller program to start and configure all that. Once installed under C:\wamp I also copied the phplint package on it and I created the BT_STATE directory. the document root where the actual web pages can be stored is c:\wamp\www. The final layout is as follows:

C:\WAMP
├───BT_STATE
│   ├───application
│   ├───sessions
│   └───users
│
├───phplint
│   └───stdlib
│       ├───com 
│       │   └───acme
│       │       └───www
│       │           ├───Dashboard.php
│       │           └───MyForm.php
│       ├───it
│       │   └───icosaedro
│       │       ├───web
│       │       │   └───bt_
│       │       └───...
│       └───...
│
└───www
    ├───index.php
    └───login.php
File system layout of my wamp server test installation.

Under the BT_STATE directory, bt_ will create the application directory (where site specific parameters can be saved), the sessions directory (where bt_ stores the active users' sessions) and the users directory (where user's specific preferences can be stored).

I have also copied the complete PHPLint package, although only its stdlib directory is really necessary to just run a web site; having PHPLint at hand may help anyway. Note that PHPLint and all its libraries lies outside the document root c:\wamp\www so nobody can directly access any data therein. And finally I created the login.php page and the dispatcher page index.php. From the users' point of view, the dispatcher page will be the only existing page on that web site, besides the login page itself. Moreover, by naming "index.php" the dispatcher, the dispatcher is automatically started each time users points their browser to the web site, whose URL is by now simply http://localhost. (The latter statement actually depends on the configuration of Apache, but normally "index.php" is marked as default index page, and WAMP server comes just already configured in this way).

The login page

The login page is a normal PHP page: it shows the usual login, then validates name and password, and finally creates the bt_ session for the user. We will not cover here all the details because these are quite common techniques you should already know. Our only interest here is just how the bt_ session gets started:

<?php
require_once __DIR__ . "/../phplint/stdlib/all.php";

use it\icosaedro\web\bt_\UserSession;

new UserSession(
	"admin",            // login name of the user
	"c:/wamp/BT_STATE", // bt state directory
	"/index.php",       // dispatcher URL
	"/login.php",       // login URL
	"com\\acme\\www\\Dashboard::enter" // dashboard function
);
The login.php page (usual password validation omitted).

All the login page has to do in order to create the session for an user named "admin" is creating an instance of the UserSession class. A new window session is then created and the indicated static method of the dashboard is invoked: the user is now seeing its personal home page. To make things simpler, UserSession also saves a copy of its instance under a static variable, so that al its static methods may recover that singleton instance while generating the page and programs may access that instance through simple static functions of the UserSession class.

Final note: bt_ heavily relies on the automatic class loading mechanism provided by the autloader module of PHPLint, which requires all the classes be precisely located under the standard directory according to their namespace. That's why it's so important to include the "all.php" module or, at least, the "autoload.php" module.

The dispatcher page

The dispatcher page index.php will receives any request from here on by logged in users and will route these requests to the proper forward call according to the current state of their bt file and according to the specific event they generated in the page:

<?php
require_once __DIR__ . "/../phplint/stdlib/all.php";

use it\icosaedro\web\bt_\UserSession;

new UserSession(
	NULL,
	"c:/wamp/BT_STATE", // bt state directory
	"/index.php",       // dispatcher URL
	"/login.php",       // login URL
	"com\\acme\\www\\Dashboard::enter" // dashboard function
);
The dispatcher page index.php.

All the dispatcher has to do is creating an instance of the UserSession class by setting a NULL user name; the remaining arguments are just the same as the login page. By setting a NULL user name, UserSession will try to resume the session according to the cookie received along with the request. If it's ok, the requested forward call is invoked.

The dashboard page

There is no much to say about the com\acme\www\Dashboard::enter() function. All it has to do is to recover the user's name with UserSession::getSessionParameter("name"), retrieve in some way the specific permissions of that user, and then display the dashboard accordingly:

<?php
namespace com\acme\www;

require_once __DIR__ . "/../../../all.php";

use it\icosaedro\web\bt_\UserSession;

class Dashboard {

	static function enter(){
		$user_name = UserSession::getSessionParameter("name");
		echo "<html><body>";
		UserSession::formOpen();
		echo "Welcome back, $user_name!";
		...
		UserSession::anchor("LOGOUT", UserSession::class . "::logout");
		UserSession::formClose();
		echo "</body></html>";
	}

}
Basic structure of the dashboard class.

Being this file nested under c:\wamp\phplint\stdlib\com\acme\www\Dashboard.php, to load the "all.php" module located under stdlib we need to go up 3 directories in order to build the right relative path starting from __DIR__. This is probably the harder thing to setup correctly. Our dashboard contains only the enter() static function that generates the landing page for any logged-in user. Here users may start their navigation into the web application. Depending on the specific role and permissions, the dashboard may display some controls but not others: there is no way for anyone to invoke a page he has not access to; only buttons and anchors explicitly made available in the page can be invoked by users. For example:

if( site management granted )
	UserSession::anchor("Site Management", "com\\acme\\www\\SiteManagement::enter");
if( site statistics granted )
	UserSession::anchor("Site Statistics", "com\\acme\\www\\SiteStatistics::enter");
if( users management granted )
	UserSession::anchor("Users Management", "com\\acme\\www\\UsersManagement::enter");
Customizing the dashboard according to user's permissions.

Logout

Simply put a button or an anchor invoking the logout function of bt_ anywhere you want in the user's page:

UserSession::anchor("LOGOUT", UserSession::class . "::logout");
Putting a logout control in the user's page.

The session on the server is deleted, the cookie on the browser is deleted as well, and the browser is sent back to the login page.

Support for multiple windows

Bt_ supports several navigation windows at once per user session. Each window session is indeed a session environments with its bt file, bt stack and specific set of custom parameters. TO create a new navigation window the user may open another tab or window in its browser and there it may invoke the despatcher page index.php, that is just the entry page of our web site. You may also put a specific link somewhere in the page to do that:

<a href="/" target=_blank>Open another window</a>

What's missing?

A concept of form, of curse, something to handle the boring task to display masks, retrieving fields, save and restore their state, and so on. Its time to look at bt forms.

→ Continue reading the bt form tutorial.


Umberto Salsi
Comments
Contact
Site map
Home / Section index
Still no comments to this page. Use the Comments link above to add your contribute.