This tutorial introduces the Prado web application framework's
+ ActiveRecord
+ and Active Controls to build a Chat
+ web application . It is assumed that you
+ are familiar with PHP and you have access to a web server that is able to serve PHP5 scripts.
+ This basic chat application will utilizing the following ideas/components in Prado.
+
+
Building a custom User Manager class.
+
Authenticating and adding a new user to the database.
+
Using ActiveRecord to interact with the database.
+
Using Active Controls and callbacks to implement the user interface.
+
Separating application logic and application flow.
+
+
+
+
In this tutorial you will build an AJAX Chat web application that allows
+ multiple users to communicate through their web browser.
+ The application consists of two pages: a login page
+ that asks the user to enter their nickname and the main application chat
+ page.
+ You can try the application locally or at
+ Pradosoft.com.
+ The main application chat page is shown bellow.
+ class="figure" />
+
+
+
Download, Install and Create a New Application
+
The download and installation steps are similar to those in
+ the Currency converter tutorial.
+ To create the application, we run from the command line the following.
+ See the Command Line Tool
+ for more details.
+
+php prado/framework/prado-cli.php -c chat
+
+
+
+
The above command creates the necessary directory structure and minimal
+ files (including "index.php" and "Home.page") to run a Prado web application.
+ Now you can point your browser's url to the web server to serve up
+ the index.php script in the chat directory.
+ You should see the message "Welcome to Prado!"
+
+
+
Authentication and Authorization
+
The first task for this application is to ensure that each user
+ of the chat application is assigned with a unique (choosen by the user)
+ username. To achieve this, we can secure the main chat application
+ page to deny access to anonymouse users. First, let us create the Login
+ page with the following code. We save the Login.php and Login.page
+ in the chat/protected/pages/ directory (there should be a Home.page
+ file there create by the command line tool).
+
The login page contains
+ a ,
+ a ,
+ a
+ and a . The resulting
+ page looks like the following (after applying some a style sheet).
+ class="figure" />
+ If you click on the Login button without entering any
+ text in the username textbox, an error message is displayed. This is
+ due to the
+ requiring the user to enter some text in the textbox before proceeding.
+
+
Securing the Home page
+
Now we wish that if the user is trying to access the main application
+page, Home.page, before they have logged in, the user is presented with
+the Login.page first. We add a chat/protected/application.xml configuration
+file to import some classes that we shall use later.
+
+
+
+
+
+
+
+
+
+
+
+Next, we add a chat/protected/pages/config.xml configuration file to
+secure the pages directory.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+We setup the authentication using the default classes as explained in the
+authentication/authorization quickstart.
+In the authorization definition, we allow anonymouse users to access the
+Login page (anonymouse users is specified by the ? question mark).
+We allow any users with role equal to "normal" (to be defined later)
+to access all the pages, that is, the Login and Home pages.
+Lastly, we deny all users without any roles to access any page. The authorization
+rules are executed on first match basis.
+
+
+
If you now try to access the Home page by pointing your browser
+to the index.php you will be redirected to the Login page.
+
+
+
Active Record for chat_users table
+
The
+class only provides a read-only list of users. We need to be able to add or
+login new users dynamically. So we need to create our own user manager class.
+First, we shall setup a database with a chat_users table and create an ActiveRecord
+that can work with the chat_users table with ease. For the demo, we
+use sqlite as our database for ease of distributing the demo. The demo
+can be extended to use other databases such as MySQL or Postgres SQL easily.
+We define the chat_users table as follows.
+
+CREATE TABLE chat_users
+(
+ username VARCHAR(20) NOT NULL PRIMARY KEY,
+ last_activity INTEGER NOT NULL DEFAULT "0"
+);
+
+Next we define the corresponding ChatUserRecord class and save it as
+chat/protected/App_Code/ChatUserRecord.php (you need to create the
+App_Code directory as well). We also save the sqlite database file
+as App_Code/chat.db.
+
+class ChatUserRecord extends TActiveRecord
+{
+ public $username;
+ public $last_activity;
+
+ public static $_tablename='chat_users';
+
+ public static function finder()
+ {
+ return parent::getRecordFinder('ChatUserRecord');
+ }
+}
+
+Before using the ChatUserRecord class we to configure a default
+database connection for ActiveRecord to function. In the chat/protected/application.xml
+we import classes from the App_Code directory and add an
+ActiveRecord configuration module.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Custom User Manager class
+
To implement a custom user manager module class we just need
+to extends the TModule class and implement the IUserManager
+interface. The getGuestName(), getUser() and validateUser
+methods are required by the IUserManager interface.
+We save the custom user manager class as App_Code/ChatUserManager.php.
+
+
+class ChatUserManager extends TModule implements IUserManager
+{
+ public function getGuestName()
+ {
+ return 'Guest';
+ }
+
+ public function getUser($username=null)
+ {
+ $user=new TUser($this);
+ $user->setIsGuest(true);
+ if($username !== null && $this->usernameExists($username))
+ {
+ $user->setIsGuest(false);
+ $user->setName($username);
+ $user->setRoles(array('normal'));
+ }
+ return $user;
+ }
+
+ public function addNewUser($username)
+ {
+ $user = new ChatUserRecord();
+ $user->username = $username;
+ $user->save();
+ }
+
+ public function usernameExists($username)
+ {
+ $finder = ChatUserRecord::finder();
+ $record = $finder->findByUsername($username);
+ return $record instanceof ChatUserRecord;
+ }
+
+ public function validateUser($username,$password)
+ {
+ return $this->usernameExists($username);
+ }
+}
+
+
+The getGuestName()
+method simply returns the name for a guest user and is not used in our application.
+The getUser() method returns a TUser object if the username
+exists in the database, the TUser object is set with role of "normal"
+that corresponds to the <authorization> rules defined in our
+config.xml file.
+
+
The addNewUser() and usernameExists()
+method uses the ActiveRecord corresponding to the chat_users table to
+add a new user and to check if a username already exists, respectively.
+
+
+
The next thing to do is change the config.xml configuration to use
+our new custom user manager class. We simply change the <module>
+configuration with id="users".
+
+
+
+
+
Authentication
+
To perform authentication, we just want the user to enter a unique
+username. We add a
+
+for validate the uniqueness of the username and add a OnClick event handler
+for the login button.
+
+<com:TCustomValidator
+ ControlToValidate="username"
+ Display="Dynamic"
+ OnServerValidate="checkUsername"
+ ErrorMessage="The username is already taken." />
+
+...
+
+<com:TButton Text="Login" OnClick="createNewUser" />
+
+In the Login.php file, we add the following 2 methods.
+
+function checkUsername($sender, $param)
+{
+ $manager = $this->Application->Modules['users'];
+ if($manager->usernameExists($this->username->Text))
+ $param->IsValid = false;
+}
+
+function createNewUser($sender, $param)
+{
+ if($this->Page->IsValid)
+ {
+ $manager = $this->Application->Modules['users'];
+ $manager->addNewUser($this->username->Text);
+
+ //do manual login
+ $user = $manager->getUser($this->username->Text);
+ $auth = $this->Application->Modules['auth'];
+ $auth->updateSessionUser($user);
+ $this->Application->User = $user;
+
+ $url = $this->Service->constructUrl($this->Service->DefaultPage);
+ $this->Response->redirect($url);
+ }
+}
+
+The checkUserName() method uses the ChatUserManager class
+(recall that in the config.xml configuration we set the
+ID of the custom user manager class as "users") to validate the username
+is not taken.
+
+
+In the createNewUser method, when the validation passes (that is,
+when the user name is not taken) we add a new user. Afterward we perform
+a manual login process:
+
+
First we obtain a TUser instance from
+our custom user manager class using the $manager->getUser(...) method.
+
Using the TAuthManager we set/update the user object in the
+ current session data.
+
Then we set/update the Application's user instance with our
+ new user object.
+
+Finally, we redirect the client to the default Home page.
+
+
+
Default Values for ActiveRecord
+
If you try to perform a login now, you will receive an error message like
+"Property 'ChatUserRecord::$last_activity' must not be null as defined
+by column 'last_activity' in table 'chat_users'.". This means that the $last_activity
+property value was null when we tried to insert a new record. We need to either
+define a default value in the corresponding column in the table and allow null values or set the default
+value in the ChatUserRecord class. We shall demonstrate the later by
+altering the ChatUserRecord with the addition of a set getter/setter
+methods for the last_activity property.
+
+
+private $_last_activity;
+
+public function getLast_Activity()
+{
+ if($this->_last_activity === null)
+ $this->_last_activity = time();
+ return $this->_last_activity;
+}
+
+public function setLast_Activity($value)
+{
+ $this->_last_activity = $value;
+}
+
+Notice that we renamed $last_activity to $_last_activity (note
+the under score after the dollar sign).
+
+
+
Main Chat Application
+
Now we are ready to build the main chat application. We use a simple
+layout that consist of one panel holding the chat messages, one panel
+to hold the users list, a textare for the user to enter the text message
+and a button to send the message.
+
+
+
+
+ Prado Chat Demo
+
+
+
+<com:TForm>
+
+</com:TForm>
+<com:TJavascriptLogger />
+
+
+
+We have add two Active Control components: a
+
+and a
+.
+We also added a
+
+that will be very useful for understanding how the Active Controls work.
+
+
+
Exploring the Active Controls
+
Lets have some fun before we proceed with setuping the chat buffering. We want
+to see how we can update the current page when we receive a message. First, we add
+an OnClick event handler for the Send button.
+
+
+<com:TActiveButton ID="sendButton" CssClass="send-button"
+ Text="Send" OnClick="processMessage"/>
+
+And the corresponding event handler method in the Home.php class (we
+need to create this new file too).
+
+class Home extends TPage
+{
+ function processMessage($sender, $param)
+ {
+ echo $this->userinput->Text;
+ }
+}
+
+If you now type something in the main application textbox and click the send button
+you should see what ever you have typed echoed in the TJavascriptLogger console.
+
+
+
To append or add some content to the message list panel, we need to use
+some methods in the
+
+class which is available through the CallbackClient property of the
+current TPage object. For example, we do can do
+
+function processMessage($sender, $param)
+{
+ $this->CallbackClient->appendContent("messages", $this->userinput->Text);
+}
+
+This is one way to update some part of the existing page during a callback (AJAX style events)
+and will be the primary way we will use to implement the chat application.
+
+
+
Active Record for chat_buffer table
+
To send a message to all the connected users we need to buffer or store
+the message for each user. We can use the database to buffer the messages. The
+chat_buffer table is defined as follows.
+
+CREATE TABLE chat_buffer
+(
+ id INTEGER PRIMARY KEY,
+ for_user VARCHAR(20) NOT NULL,
+ from_user VARCHAR(20) NOT NULL,
+ message TEXT NOT NULL,
+ created_on INTEGER NOT NULL DEFAULT "0"
+);
+
+The corresponding ChatBufferRecord class is saved as
+App_Code/ChatBufferRecord.php.
+
+
+class ChatBufferRecord extends TActiveRecord
+{
+ public $id;
+ public $for_user;
+ public $from_user;
+ public $message;
+ private $_created_on;
+
+ public static $_tablename='chat_buffer';
+
+ public function getCreated_On()
+ {
+ if($this->_created_on === null)
+ $this->_created_on = time();
+ return $this->_created_on;
+ }
+
+ public function setCreated_On($value)
+ {
+ $this->_created_on = $value;
+ }
+
+ public static function finder()
+ {
+ return parent::getRecordFinder('ChatBufferRecord');
+ }
+}
+
+
+
+
Chat Application Logic
+
We finally arrive at the guts of the chat application logic. First, we
+need to save a received message into the chat buffer for all the
+current users. We add this logic in the ChatBufferRecord class.
+
+
+public function saveMessage()
+{
+ foreach(ChatUserRecord::finder()->findAll() as $user)
+ {
+ $message = new self;
+ $message->for_user = $user->username;
+ $message->from_user = $this->from_user;
+ $message->message = $this->message;
+ $message->save();
+ if($user->username == $this->from_user)
+ {
+ $user->last_activity = time(); //update the last activity;
+ $user->save();
+ }
+ }
+}
+
+We first find all the current users using the ChatUserRecord finder
+methods. Then we duplicate the message and save it into the database. In addition,
+we update the message sender's last activity timestamp. The above piece of code
+demonstrates the simplicty and succintness of using ActiveRecords for simple database designs.
+
+
+
The next piece of the logic is to retreive the users messages from the buffer.
+We simply load all the messages for a particular username and format that message
+appropriately (remember to escape the output to prevent Cross-Site Scripting attacks).
+After we load the messages, we delete those loaded messages and any older
+messages that may have been left in the buffer.
+
";
+}
+
+
+To retrieve a list of current users (formatted), we add this logic to the
+ChatUserRecord class. We delete any users that may have been inactive
+for awhile.
+
+public function getUserList()
+{
+ $this->deleteAll('last_activity < ?', time()-300); //5 min inactivity
+ $content = '
';
+ foreach($this->findAll() as $user)
+ $content .= '
'.htmlspecialchars($user->username).'
';
+ $content .= '
';
+ return $content;
+}
+
+
+
Note:
+For simplicity
+we formatted the messages in these Active Record classes. For large applications,
+these message formatting tasks should be done using Prado components (e.g. using
+a TRepeater in the template or a custom component).
+
+
+
+
Putting It Together
+
Nows comes to put the application flow together. In the Home.php we update
+the Send buttons OnClick event handler to use the application
+logic we just implemented.
+
+function processMessage($sender, $param)
+{
+ if(strlen($this->userinput->Text) > 0)
+ {
+ $record = new ChatBufferRecord();
+ $record->message = $this->userinput->Text;
+ $record->from_user = $this->Application->User->Name;
+ $record->saveMessage();
+
+ $this->userinput->Text = '';
+ $messages = $record->getUserMessages($this->Application->User->Name);
+ $this->CallbackClient->appendContent("messages", $messages);
+ $this->CallbackClient->focus($this->userinput);
+ }
+}
+
+We simply save the message to the chat buffer and then ask for all the messages
+for the current user and update the client side message list using a callback
+response (AJAX style).
+
+
+
At this point the application is actually already functional, just not very
+user friendly. If you open two different browser, you should be able to communicate
+between the two users when ever the Send button is clicked.
+
+
+
The next part is perphaps the more tricker and fiddly than the other tasks. We
+need to improve the user experience. First, we want a list of current users
+as well. So we add the following method to Home.php, we can call
+this method when ever some callback event is raised, e.g. when the Send
+button is clicked.
+
+protected function refreshUserList()
+{
+ $lastUpdate = $this->getViewState('userList','');
+ $users = ChatUserRecord::finder()->getUserList();
+ if($lastUpdate != $users)
+ {
+ $this->CallbackClient->update('users', $users);
+ $this->setViewstate('userList', $users);
+ }
+}
+
+
+
+
Actually, we want to periodically update the messages and user list as new
+users join in and new message may arrive from other users. So we need to refresh
+the message list as well.
+
+function processMessage($sender, $param)
+{
+ ...
+ $this->refreshUserList();
+ $this->refreshMessageList();
+ ...
+}
+
+protected function refreshMessageList()
+{
+ //refresh the message list
+ $finder = ChatBufferRecord::finder();
+ $content = $finder->getUserMessages($this->Application->User->Name);
+ if(strlen($content) > 0)
+ {
+ $anchor = (string)time();
+ $content .= "";
+ $this->CallbackClient->appendContent("messages", $content);
+ $this->CallbackClient->focus($anchor);
+ }
+}
+
+The anchor using time() as ID for a focus point is so that when the
+message list on the client side gets very long, the focus method will
+scroll the message list to the latest message (well, it works in most browsers).
+
+
+
Next, we need to redirect the user back to the login page if the user has
+been inactive for some time, say about 5 mins, we can add this check to any stage
+of the page life-cycle. Lets add it to the onLoad() stage.
+
+public function onLoad($param)
+{
+ $username = $this->Application->User->Name;
+ if(!$this->Application->Modules['users']->usernameExists($username))
+ {
+ $auth = $this->Application->Modules['auth'];
+ $auth->logout();
+
+ //redirect to login page.
+ $this->Response->Redirect($this->Service->ConstructUrl($auth->LoginPage));
+ }
+}
+
+
+
+
Improving User Experience
+
The last few details are to periodically check for new messages and
+refresh the user list. We can accomplish this by polling the server using a
+
+control. We add a TTimeTriggeredCallback to the Home.page
+and call the refresh handler method to defined in Home.php.
+We set the polling interval to 2 seconds.
+
+<com:TTimeTriggeredCallback OnCallback="refresh"
+ Interval="2" StartTimerOnLoad="true" />
+
+
+function refresh($sender, $param)
+{
+ $this->refreshUserList();
+ $this->refreshMessageList();
+}
+
+
+
+
The final piece requires us to use some javascript. We want that when the
+user type some text in the textarea and press the Enter key, we want it
+to send the message without clicking on the Send button. We add to the
+Home.page some javascript.
+
+
+<com:TClientScript>
+Event.observe($("<%= $this->userinput->ClientID %>"), "keypress", function(ev)
+{
+ if(Event.keyCode(ev) == Event.KEY_RETURN)
+ {
+ if(Event.element(ev).value.length > 0)
+ new Prado.Callback("<%= $this->sendButton->UniqueID %>");
+ Event.stop(ev);
+ }
+});
+</com:TClientScript>
+
+Details regarding the javascript can be explored in the
+Introduction to Javascript section of the quickstart.
+
+
+
This completes the tutorial on making a basic chat web application using
+the Prado framework. Hope you have enjoyed it.
+
+
+
\ No newline at end of file
diff --git a/demos/quickstart/protected/pages/Tutorial/CurrencyConverter.page b/demos/quickstart/protected/pages/Tutorial/CurrencyConverter.page
index 0e54fbc2..fdce0b47 100644
--- a/demos/quickstart/protected/pages/Tutorial/CurrencyConverter.page
+++ b/demos/quickstart/protected/pages/Tutorial/CurrencyConverter.page
@@ -11,12 +11,12 @@
relative to the dollar. The completed application is shown bellow.
class="figure" />
You can try the application locally or at
- Pradosoft.com.
+ Pradosoft.com.
Notice that the application still functions exactly the same if javascript
is not available on the user's browser.
-
Downloading and Installing Prado
+
Downloading and Installing Prado
To install Prado, simply download the latest version of Prado from
http://www.pradosoft.com
and unzip the file to a directory not accessible by your web server
diff --git a/demos/quickstart/protected/pages/Tutorial/chat1.png b/demos/quickstart/protected/pages/Tutorial/chat1.png
new file mode 100644
index 00000000..8288b496
Binary files /dev/null and b/demos/quickstart/protected/pages/Tutorial/chat1.png differ
diff --git a/demos/quickstart/protected/pages/Tutorial/chat2.png b/demos/quickstart/protected/pages/Tutorial/chat2.png
new file mode 100644
index 00000000..97cbc51d
Binary files /dev/null and b/demos/quickstart/protected/pages/Tutorial/chat2.png differ
--
cgit v1.2.3