Skip to content
Leonardo M. Ramé edited this page Mar 3, 2012 · 11 revisions

Usually ExtJs applications interact with back end servers that provide Json or XML data. Those servers can be created in virtually any programming language, such as PHP, Perl, Java, Ruby, C++, Pascal, and even Bash. I'll use Lazarus, the IDE and framework for the FreePascal compiler.

I'll assume you understand the basic concepts behind FreePascal and Lazarus, and that you have read and tested the examples provided in this wiki.

Remark. It is important that you install the package Weblaz in Lazarus. If you don't know how to install a package, please read this. In Lazarus, go to Package->Install/Deinstall packages..., find "weblaz" on the bottom of the right side list, select it and click "install selection", then click on Save and Rebuild IDE.

Introducing Weblaz

Weblaz is a package that allows us to create web applications, such as CGI, FastCGI, Apache modules and Stand Alone webservers. It allows the inclusion of Webmodules, that lets divide the application in parts. Each WebModule has a list of "Actions" that can be called from the web browser as GET and POST methods.

Example

Imagine a basic CGI application called "test.cgi" running on an Apache web server. To reach this application, the user should type this on the web browser:

http://localhost/cgi-bin/test.cgi

Now, imagine the application can handle requests related to two kinds of data, for example customer data and user data. Instead of writing one class or unit containing methods to handle both types of data, theres a clean approach, the use of one WebModule for Customer data and other WebModule for User data.

If there are two web modules, one named customers and other named users, to reach them from the browser, the user should type:

http://localhost/cgi-bin/test.cgi/customers 
http://localhost/cgi-bin/test.cgi/users

Now, imagine you want to add CRUD operations to customers. In the customer webmodule just start adding actions, such as "insertCustomer", "updateCustomer", "deleteCustomer", "getCustomer" and "getCustomerList". The same for users.

To reach those methods, in the web browser you should type this:

http://localhost/cgi-bin/test.cgi/customers/getCustomerList
http://localhost/cgi-bin/test.cgi/customers/getCustomer
http://localhost/cgi-bin/test.cgi/customers/insertCustomer
http://localhost/cgi-bin/test.cgi/customers/deleteCustomer
http://localhost/cgi-bin/test.cgi/customers/updateCustomer

Creating the basic framework

In this stage, I'll explain how to create a basic cgi application, with one Webmodule and two basic actions that return Json data to be handled by our ExtJs application.

To start, please open Lazarus and go to Project->New project, then select "CGI Application" (this will be shown only if you installed the Weblaz package", then click Ok. This will create the basic structure of our project, please go to Project->Save Project as... and choose a directory to store the source files, then set the name for the project as "ar_better_crm.lpi". After saving the project's name, we are asked for defining a name for the file "unit1.pas", this file will be the handler for Customer related requests, so, please name it "customer_handler.pas".

Here is a screenshot of the files after saved:

project overview

To compile the project, just press CTRL+F9 or Execute->Build. The result will be a couple of .o, .ppu, .compiled files, and an executable called "a_better_crmlpi" on Linux/Unix or "a_better_crmlpi.exe" on Windows. I don't like the name that Lazaus created for the executable, so, please go to Project->Project Options...->Compiler Options->Name of final file (-o) and write "a_better_crm", then click Ok.

Again, CTRL+F9 and look at the generated file list, should be a "a_better_crm" file created.

Ok, now it's time to deploy the executable to our web server, you can copy the executable to /var/www/cgi-bin or create a symlink, to avoid copying every time the app is compiled.

To create a symlink, please do this (on linux):

sudo ln -s /current/path/a_better_crm /var/www/cgi-bin

Instead of /current/path you have to type in the complete path to your executable file.

Now it's time to test the results, please open your web browser and type http://localhost/cgi-bin/a_better_crm, the result should be this:

results

Now, go back to Lazarus and take a look at the file "customer_handler.pas". It should look as shown in this screenshot.

lazarus

As the screenshot shows, there's a class named "TFPWebModule1", this is the default name Lazarus creates for our WebModules, to rename it, please open the "Object Inspector" (View->Object Inspector) and select FPWebModule1: TFPWebModule1 and edit its Name property, I'll name it "CustomerHandler". The code should change to this:

Code view

unit customer_handler;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, Classes, httpdefs, fpHTTP, fpWeb; 

type
  TCustomerHandler = class(TFPWebModule)
  private
    { private declarations }
  public
    { public declarations }
  end; 

var
  CustomerHandler: TCustomerHandler;

implementation

{$R *.lfm}

initialization
  RegisterHTTPModule('TFPWebModule1', TCustomerHandler);
end.

Now its time to take a look at the "initialization" section of this code. The RegisterHTTPModule procedure defines how the module has to be referenced, here the default name is "TFPWebModule1", so the user has to point the browser to:

http://localhost/cgi-bin/a_better_crm/TFPWebModule1

As this module should contain all customer related actions, I'll replace "TFPWebModule1" to "customers" (all lowercase), please do that.

Now, again go to the Object Inspector, click CustomerHandler and look at the properties, this time click on the Events tab, then double click on the OnRequest handler, the method DataModuleRequest will be automatically created, edit the method to look like this:

procedure TCustomerHandler.DataModuleRequest(Sender: TObject;
  ARequest: TRequest; AResponse: TResponse; var Handled: Boolean);
begin
  AResponse.Content := 'Hello World!';
  Handled := True;
end; 

Now recompile, then point your browser to:

http://localhost/cgi-bin/a_better_crm/customers

The "Hello World!" message should appear, if not, please review all the steps.

Adding actions

As I mentioned before, actions are handlers for GET and POST http requests. To create an action, please go to the Object Inspector->CustomerHandler->Actions and click the "..." button, a Dialog with the options to Add or Remove actions will appear, click Add. An action named "TFPWebAction0" will be created, please rename it to "getCustomerList" (case sensitive), and click on Events tab and double click on OnRequest, this will create the method "getCustomerListRequest", please edit like this:

procedure TCustomerHandler.getCustomerListRequest(Sender: TObject;
  ARequest: TRequest; AResponse: TResponse; var Handled: Boolean);
begin
  AResponse.Content := 'Hello, I am getCustomerList action!';
  Handled := Truwe;
end;

Before testing in the web browser, please change the DataModuleRequest action to this:

procedure TCustomerHandler.DataModuleRequest(Sender: TObject;
  ARequest: TRequest; AResponse: TResponse; var Handled: Boolean);
begin
  AResponse.Content := 'Hello World!';
  Handled := False;
end;

Or just remove it.

Now, compile, then point your browser to:

http://localhost/cgi-bin/a_better_crm/customers/getCustomerList

You should see the "Hello, I am getCustomerList action!" message, if not, please double check every step.

Generating JSON content

In the Sencha Designer section I explained to you to create a static json file in the http server's documentRoot, now please move that file to /var/www/data.json to /var/www/cgi-bin/data.json, to let the CGI program acces it.

After the file is moved, please add the units fpJson and jsonparser to the "uses" clause of customer_handler.pas, and re-edit the getCustomerListRequest method to look like this:

Code view

procedure TCustomerData.getCustomerListRequest(Sender: TObject; ARequest: TRequest;
  AResponse: TResponse; var Handled: Boolean);
var
  lJSonParser: TJSONParser;
  lStr: TFileStream;
  lFile: string;
  lJSON: TJSONObject;

begin
  lFile := 'data.json';
  lStr := TFileStream.Create(lFile, fmOpenRead);
  lJSonParser := TJSONParser.Create(lStr);
  try
    lJSON := lJSonParser.Parse as TJSonObject;
    AResponse.Content := lJSON.AsJSON;
  finally
    lJSonParser.Free;
    lStr.Free;
  end;
  Handled := True;
end;

Again, compile and test in your web browser, the result should be this:

json

Note. If your json data includes utf8 characters, please add this before AResponse.Content:

AResponse.ContentType := 'text/html;charset=utf-8';

To start using the CGI provided data, instead of the static file, please open your project on Sencha Designer, then go to Project Inspector->Stores->Customers->MyAjaxProxy and change the "url" property to:

/cgi-bin/a_better_crm/customers/getCustomerList

Save, Deploy and test.

Handling updates

The url parameter we've set in the previous paragraph, allows to read data from the server, but what about Insert/Update and Delete?.

The ajax proxy provides a nice method to handle CRUD operations, to use it, just paste this in the "api" property of MyAjaxProxy, and later clean the url property:

MyAjaxProxy->api:

{
read: "/cgi-bin/a_better_crm/customers/read",
create: "/cgi-bin/a_better_crm/customers/insert",
update: "/cgi-bin/a_better_crm/customers/update",
destroy: "/cgi-bin/a_better_crm/customers/delete"
}

Important!. On Windows you must replace /cgi-bin/a_better_crm/customers/... with /cgi-bin/a_better_crm.exe/customers/...

As you can see, when the Store.load() method is called, the url /cgi-bin/a_better_crm/customers/read is executed. As we don't have a "read" method on our "customers" WebModule, we'll rename "getCustomerList" action to just "read".

Now, to create an Update handler, just add a new action in your WebModule. Let's call it "update" with an OnRequest event handler also named "update", then add this code:

Code view

procedure TCustomerHandler.update(Sender: TObject; ARequest: TRequest;
  AResponse: TResponse; var Handled: Boolean);
var
  lResponse: TJSONObject;
  lJSON: TJSONObject;
  lResult: TJSONObject;
  lArray: TJSONArray;
  lJSonParser: TJSONParser;
  lStr: TStringList;
  lFile: string;
  I: Integer;
begin
  (* Load "database" *)
  lFile := 'data.json';
  lStr := TStringList.Create;
  lStr.LoadFromFile(lFile);
  lJSonParser := TJSONParser.Create(lStr.Text);
  lResponse :=TJSONObject.Create;
  try
    try
      (* Assign values to local variables *)
      lResult := TJSONParser.Create(ARequest.Content).Parse as TJSONObject;

      (* Traverse data finding by Id *)
      lJSON := lJSonParser.Parse as TJSonObject;
      lArray := lJSON.Arrays['root'];
      for I := 0 to lArray.Count - 1 do
      begin
        if lResult.Integers['id'] = (lArray.Objects[I] as TJsonObject).Integers['id'] then
        begin
          (* Assign field values *)
          (lArray.Objects[I] as TJsonObject).Strings['name'] := lResult.Strings['name'];
          (lArray.Objects[I] as TJsonObject).Strings['email'] := lResult.Strings['email'];
          (lArray.Objects[I] as TJsonObject).Integers['age'] := lResult.Integers['age'];
          (lArray.Objects[I] as TJsonObject).Integers['gender'] := lResult.Integers['gender'];
          (lArray.Objects[I] as TJsonObject).Booleans['active'] := lResult.Booleans['active'];
          lStr.Text := lJSon.AsJSON;
          lStr.SaveToFile(lFile);
          Break;
        end;
      end;
      lResponse.Add('success', true);
      lResponse.Add('root', lResult);
      AResponse.ContentType := 'text/json;charset=utf-8';
      AResponse.Content := lResponse.AsJSON;
    except
      on E: Exception do
      begin
        lResponse.Add('success', false);
        lResponse.Add('msg', E.Message);
        AResponse.Content := lResponse.AsJSON;
      end;
    end;
  finally
    lResponse.Free;
    lJSonParser.Free;
    lStr.Free;
  end;

  Handled := True;
end;

This looks like a bunch of code, and it is, because we are handling all the file access directly. You can improve this code by replacing the data.json file by something more practical, such an SQL database.

Now go back to Sencha Designer and edit the action onOkClick of CustomerProperties controller with this code:

Code view

onOkClick: function(button, e, options) {
    var win = button.up('window');
    frm = win.down('form').getForm();
    var store = this.getCustomersStore();    
    if(mode=="Insert") {
      customer = frm.getValues();
      store.insert(0, customer);
    }
    else
    { 
      customer = frm.getRecord();
      frm.updateRecord(customer);
    }
    store.sync();
    win.destroy();
}

In Lazarus, let's add a new action to the customers WebModule, the new action will be called "insert" and the OnRequest event will be also called "insert".

Code view

procedure TCustomerHandler.insert(Sender: TObject; ARequest: TRequest;
  AResponse: TResponse; var Handled: Boolean);
var
  lResponse: TJSONObject;
  lJSON: TJSONObject;
  lResult: TJSONObject;
  lArray: TJSONArray;
  lJSonParser: TJSONParser;
  lStr: TStringList;
  lFile: string;

begin
  Randomize;
  (* Load "database" *)
  lFile := 'data.json';
  lStr := TStringList.Create;
  lStr.LoadFromFile(lFile);
  lJSonParser := TJSONParser.Create(lStr.Text);
  lResponse :=TJSONObject.Create;
  try
    try
      lJSON := lJSonParser.Parse as TJSonObject;
      lArray := lJSON.Arrays['root'];
      (* Assign values to local variables *)
      lResult := TJSONParser.Create(ARequest.Content).Parse as TJSONObject;
      lResult.Integers['id'] := Random(1000);
      lArray.Add(lResult);
      (* Save the file *)
      lStr.Text:= lJSON.AsJSON;
      lStr.SaveToFile(lFile);

      lResponse.Add('success', true);
      lResponse.Add('root', lResult);

      AResponse.Content := lResponse.AsJSON;
    except
      on E: Exception do
      begin
        lResponse.Add('success', false);
        lResponse.Add('msg', E.Message);
        AResponse.Content := lResponse.AsJSON;
      end;
    end;
  finally
    lResponse.Free;
    lJSonParser.Free;
    lStr.Free;
  end;
  Handled := True;
end;

To implement the Delete action, do the same process of adding a new action called "delete" with an OnRequest handler named "delete" with this code:

Code view

procedure TCustomerHandler.delete(Sender: TObject; ARequest: TRequest;
  AResponse: TResponse; var Handled: Boolean);
var
  lResponse: TJSONObject;
  lJSON: TJSONObject;
  lResult: TJSONObject;
  lArray: TJSONArray;
  lJSonParser: TJSONParser;
  lStr: TStringList;
  lFile: string;
  I: Integer;

begin
  (* Load "database" *)
  lFile := 'data.json';
  lStr := TStringList.Create;
  lStr.LoadFromFile(lFile);
  lJSonParser := TJSONParser.Create(lStr.Text);
  lResponse :=TJSONObject.Create;
  try
    try
      (* Assign values to local variables *)
      lResult := TJSONParser.Create(ARequest.Content).Parse as TJSONObject;

      (* Traverse data finding by Id *)
      lJSON := lJSonParser.Parse as TJSonObject;
      lArray := lJSON.Arrays['root'];
      for I := 0 to lArray.Count - 1 do
      begin
        if lResult.Integers['id'] = (lArray.Objects[I] as TJsonObject).Integers['id'] then
        begin
          lArray.Delete(I);
          lStr.Text := lJSon.AsJSON;
          lStr.SaveToFile(lFile);
          Break;
        end;
      end;
      lResponse.Add('success', true);
      lResponse.Add('root', lResult);
      AResponse.Content := lResponse.AsJSON;
    except
      on E: Exception do
      begin
        lResponse.Add('success', false);
        lResponse.Add('msg', E.Message);
        AResponse.Content := lResponse.AsJSON;
      end;
    end;
  finally
    lResponse.Free;
    lJSonParser.Free;
    lStr.Free;
  end;

  Handled := True;
end;

Also, in Sencha Designer, please review the CustomerGrid controller and its onDeleteClick method:

Code view

onDeleteClick: function(button, e, options) {
    Ext.Msg.show({
        title:'Delete record?',
        msg: 'Please configrm',
        buttons: Ext.Msg.YESNO,
        icon: Ext.Msg.QUESTION,
        fn: function(btn, text) {
            if(btn == 'yes') {
                record =  button.up('gridpanel').getSelectionModel().getSelection()[0];
                var store = this.getCustomersStore();
                store.remove(record);
                store.sync();
            }
        },
        scope: this
    });
}

Implementing the Login handler

Until now, I've explained how to create a module named "customers" that handles customer CRUD opperations. Now I'll explain how to handle the Login operation.

Please, in Lazarus go to File->New...->Module->Web Module. This will create a new unit called "unit1" or something similar, please go to File->Save... and save this unit as "login_handler".

After the new unit is created, go to the Object Inspector and rename the new module called "TFPWebModule1" to "LoginHandler". Then go to the bottom of the source file and replace the RegisterHTTPModule directive to:

RegisterHTTPModule('login', TLoginHandler);

Then, go back to the Object Inspector and click on LoginHandler, then click on Actions, this will open the Actions dialog, please add a new action and set its Name property to "check", then go to Events and double click at the right of "OnRequest" to create the checkRequest handler.

The content of the new event is this:

Code View

procedure TLoginHandler.checkRequest(Sender: TObject; ARequest: TRequest;
  AResponse: TResponse; var Handled: Boolean);
var
  lJSON: TJSONObject;

begin
  try
    lJSON := TJSONObject.Create;
    if (ARequest.ContentFields.Values['userName']='admin') and (ARequest.ContentFields.Values['passWord']='admin') then
    begin
      lJSON.Add('success', True);
    end
    else
    begin
      lJSON.Add('failure', True);
      lJSON.Add('msg', 'Incorrect User or Password.');
    end;
    AResponse.ContentType := 'application/json; charset=utf-8';
    AResponse.Content := AnsiToUtf8(lJSON.AsJSON);
  finally
    lJSON.Free;
  end;
  Handled := True;
end;

This event receives form data sent by the ExtJs application, and checks for the values of "userName" and "passWord" fields. If they are "admin" and "admin", it returns a "success" value, if not, it returns a "failure" with a message.

That's it.

The new unit should look like this:

Code View

unit login_handler;

{$mode objfpc}{$H+}

interface

uses
  SysUtils, Classes, httpdefs, fpHTTP, fpWeb, fpjson;

type

  { TLoginHandler }

  TLoginHandler = class(TFPWebModule)
    procedure checkRequest(Sender: TObject; ARequest: TRequest;
      AResponse: TResponse; var Handled: Boolean);
  private
    { private declarations }
  public
    { public declarations }
  end;

var
  LoginHandler: TLoginHandler;

implementation

{$R *.lfm}

{ TLoginHandler }

procedure TLoginHandler.checkRequest(Sender: TObject; ARequest: TRequest;
  AResponse: TResponse; var Handled: Boolean);
var
  lJSON: TJSONObject;

begin
  try
    lJSON := TJSONObject.Create;
    if (ARequest.ContentFields.Values['userName']='admin') and (ARequest.ContentFields.Values['passWord']='admin') then
    begin
      lJSON.Add('success', True);
    end
    else
    begin
      lJSON.Add('failure', True);
      lJSON.Add('msg', 'Incorrect User or Password.');
    end;
    AResponse.ContentType := 'application/json; charset=utf-8';
    AResponse.Content := AnsiToUtf8(lJSON.AsJSON);
  finally
    lJSON.Free;
  end;
  Handled := True;
end;

initialization
  RegisterHTTPModule('login', TLoginHandler);
end.

Now, please go to Sencha Designer and click on the Login controller, then find its "onLoginClick" event, and replace the old content with this:

Code View

var win = button.up('loginform');
var frm = win.getForm();
frm.submit({
    success: function(form, action){
        console.log('success');
        var UserController = this.getController('MyApp.controller.User');
        this.getController('MyApp.controller.Main').showMainView();
        UserController.saveSession(); 
        win.destroy();
    },
    failure: function(form, action){
        switch(action.failureType){
            case Ext.form.Action.CLIENT_INVALID:
                Ext.Msg.alert('Failure', 'Please complete the required fields.');
                break;
            case Ext.form.Action.CONNECT_FAILURE:
                Ext.Msg.alert('Failure', 'Ajax communication failed.');
                break;
            case Ext.form.Action.SERVER_INVALID:
                Ext.Msg.alert('Failure', action.result.msg);
                break;                
        }
    },
    scope: this
});

After this, you'll have to point the "url" property of the LoginForm View to: /cgi-bin/a_better_crm/login/check.

This was the last part of the tutorial. I hope you enjoyed and learned as much as I did writing it.

Leonardo.