Tuesday, 28 June 2011

CakePHP: Turn an input form into a query form (Version 2)

CakePHP makes constructing input forms easy. The Form helper object's input() methods provides us with the necessary intelligence, to display and get input data from our users, then store it into the controller's $this->data property using the format required by the framework's Model->save() method, so they can eventually be written to the database, with no need for any elaborate code. Furthermore, given the fact that cake's cake script will bake all that standard code in a flash. it becomes a matter of minutes to create a full featured CRUD application, starting from just a good database design.

One thing I find missing from the baked code, is the ability to create input forms whose data will be used as search criteria -- in a QBE fashion -- in an index or display page. My solution requires us to convert the submitted data into a form that is compatible with the Model->find()'s $conditions parameter and then feed this to the Model->find() or the Controller->paginate() methods in order to limit the number of returned items.

As Richard pointed out during the first version of this post, cake offers the postConditions() method, which does this out of the box and the truth of the matter is that I had completely missed that when I started coding. After looking at the CakePHP code and comparing it with my approach, I can only say that with the code below, you don't have to worry about operators and providing complex parameters. The latest version will allow users to type their desired operator before the value in a QBE like fashion. The standard Cake approach gives more accurate control over the entire process, but I am not sure how to create a UI that will allow users to change operators dynamically .

So, to get started with my alternative version, the typical index() method, for a model named Product, using this approach will have to look somehow like this:

    function index()
    {        
        $this->Product->recursive = 0;
        $conditions = $this->getSearchConditions($this->Product->name);
        $products = $this->paginate('Product', $conditions);

        $this->set( 'products', $products);
        $this->prepareCombos();
    }

The important part here s obviously the $this->getSearchConditions() function that will get the controllers $data property and transform it into the $conditions array. The perfect place to put the code for that would be our application's controller.

class AppController extends Controller {
    const CONDITIONS_SESSION_KEY = 'SRCH_COND';
    const FORM_DATA_SESSION_KEY = 'SRCH_DATA';

    private $SQL_OPERATORS = array(
        'IN', '<>', '>=', '<=',
        '>', '<'        
    );

    /**
     * @name: getSearchConditions()
     * @access: protected
     * @author: Thanassis Bakalidis
     * @param : string $modelName name of the model to search controller data
     * Return an array to be used as search conditions in a Model's find 
     * method based on the controller's current data
     * @version: 1.4
     */
    protected function getSearchConditions($modelName = null)
    {
        if ($modelName == null)
            return null;

        // create speciffic keys for the model andcontroller
        $sessionConditionsKey = sprintf("%s-%s-%s",
                                self::CONDITIONS_SESSION_KEY,
                                $this->name,
                                $modelName
                            );
        $sessionDataKey = sprintf("%s-%s-%s",
                                self::FORM_DATA_SESSION_KEY,
                                $this->name,
                                $modelName
                            );

        if (empty($this->data)) {
            // attempt to read conditions from sesion
            $conditions = $this->Session->check($sessionConditionsKey)
                ? $this->Session->read($sessionConditionsKey)
                : array();
            $this->data = $this->Session->check($sessionDataKey)
                ? $this->Session->read($sessionDataKey)
                : array();
        } else {
            // we have posted data. Atempt to rebuild conditons
            // array
            $conditions = array();
            foreach( $this->data[$modelName] as $key => $value) {
                if (empty($value))
                    continue;

                $operator = $this->extractOperator($value);
                
                if (is_array($value)) {
                    // this can only be a date field

                    $month = $value['month'];
                    $day = $value['day'];
                    $year = $value['year'];

                    // We want all three variables to be numeric so we 'll check their
                    // concatenation. After all PHP numbers as just strings with digits
                    if (is_numeric($month.$day.$year) && checkdate( $month, $day, $year)) {
                        $conditionsKey ="$modelName.$key";
                        $conditionsValue = "$year-$month-$day";
                    } else
                        continue;                        
                } else {
                    // we have normal input check the operator given                    
                    if ($operator == '' && !is_numeric($value)) {
                        // turn '=' to 'LIKE' for non numeric data
                        // numeric data will be treated as if they
                        // have an wquals operator
                        $operator = 'LIKE';
                        $value = str_replace('*', '%',  $value);
                        $value = str_replace('?', '.',  $value);                        
                    } else if ($operator === 'IN') {
                        // we need to convert the input string to an aray
                        // of the designated values
                        $operator = '';
                        $value = array_filter(explode( ' ', $value));
                    } 
                    
                    $conditionsValue = $value;
                    $conditionsKey = "$modelName.$key $operator";                    
                }

                // add the new condition entry
                $conditions[trim($conditionsKey)] = $conditionsValue;
            }

            // if we have some criteria, add them in the sesion
            $this->Session->write($sessionConditionsKey, $conditions);
            $this->Session->write($sessionDataKey, $this->data);
        }

        return $conditions;
    }

    private function extractOperator(&$input)
    {     
        $operator = strtoupper((strtok($input, ' '));
        
        if (in_array($operator, $this->SQL_OPERATORS)) {
            $opLength = strlen($operator);
            $inputLength = strlen($input);
            $input = trim(substr( $input, $opLength, $inputLength - $opLength));                        
        } else {            
            $operator = '';
        }
        
        return $operator;
    }

This was the hard part. The next ones have to do with only copy and paste. So get the form that cake created from your add or edit views and copy it before the table in the index view page. You may possibly wish to remove some unwanted input fields and change the value of the $this->Form->end() function's parameter from "Submit" to "Search".

So we are almost there. One last stroke and we are ready. Remember the $this->prepareCombos(); last comand on the index action? Well, that is a simple private method I created by copying all the find)'list', ...) commands that the cake bake script created after the add() and edit() methods, so that all foreign key fields have correct value ranges, when the input form gets displayed. For my product's controller for example the function looks like this:

    private function prepareCombos()
    {
        $productTypes = $this->Product->ProductType->find('list');
        $productCategories = $this->Product->ProductCategory->find('list');
        $suppliers = $this->Product->Supplier->find(
                                                'list',
                                                array(
                                                    'order' => array(
                                                        'id' => 'asc'
                                                    )
                                                )
                                            );
        $qualities = $this->Product->Quality->find('list');        
        $this->set(compact('productTypes', 'productCategories', 'suppliers', 'qualities'));
    }

Notes

  • In case your model contains validation rules like range then the default input element that the form helper creates , will contain an appropriate maxlength attribute, that will not allow your users to type more than the maximum allowed characters for the field. Say, for example that you have a rule for a field named width, that looks like this
           'width' => array (
                'rule' => array('range', 1, 999),
                'message' => 'Width must be between 1 and 999 mm',
                'last' => true
           )
          
    ... then the actual HTML input element will have a maxlength attribute of 3. This is not going to make your query UI very usable, so make sure that the search form's echo $this->Form->input( ... statement provides an appropriate hardcoded value for maxlength.
  • The next thing to consider, is what will happen when our users provide wrong inputs, say they type =< 13 instead of >= 13. In that case the code -- as it is now -- will create an SQL statement of the form select ... where Model.Field LIKE '=< 13' , that will provide an empty result set, but is still valid SQL. Any workaround for this type of situation would make our code even more complicated than it is now, so I am not going to spend any more thought on this.
  • After going this far, I believe that the next reasonable thing would be to move the functionality into a new component, that will take away all the complexity from the AppController class, but this is probably going to be the subject of a forthcoming post.

Tuesday, 7 June 2011

CakePHP: Authenticating against Microsoft Active Directory

My work environment uses Windows. Everyone logs on to an Active Directory based domain and this is how security is managed. When you have web applications running on Linux and you need some sort of authentication service, then you can either implement your own user data store or you can use an exiting. The good news is that MS AD is actually an LDAPv3 server, so getting data to and from LDAP is not that difficult in the world of both PHP and Cake.

The following "howto" will list the steps I followed in order to perform authentication from the company's Windows 2003 servers. I will also explain one approach for deciding how to grant users admin privileges on a CakePHP application based on membership of a Windows user group. Many might argue that I am oversimplifying, but I believe that on most occasions, this will be more than adequate.

In order to get started, remember that building an authentication system on a CakePHP application starts with creating a User model. Only difference will be here that the model in question will not get its data from an ordinary database table, but rather from an LDAP data source.

Get the LDAP datasource component

So this brings as to the first issue, which is: Get a datasource to read LDAP data. Fortunately the bakery has just what we need. The actual article that contains the code I am still using is this.

Analogrithems, however, the author of the datasource code, is providing new updates on github accessible via this link. The truth is that I have yet to test the new version, so if you are to follow the tutorial better stick with the one in the bakery.

No matter the version you choose you will end up with a file named ldap_source.php, that you will need to place in your APP/models/datasources/ directory.

Set up a connection using the new datasource

The next thing that needs to be done is to create a new connection that will use the new datasource. Open APP/config/database.php and add a new entry looking more or less like this

    var $ldap = array (
        'datasource' => 'ldap',
         // list of active directory hosts
        'host' => array(
                    'ldap1.example.com',
                    'ldap2.example.com',
                    'ldap3.example.com'
                ),
        'port' => 389,
         // location (root) in LDAP tree to start searching 
        'basedn' => 'DC=example,DC=com',
         // user to authenticate on AD server with
        'login' => 'cn=LDAPBindUser,cn=Users,dc=example,dc=com',
        'password' => 'abABa@332',
        'database' => '',
        'tls'      => false,
        'version'  => 3
    );

Verify with your network administrator that LDAPBindUser in created on the correct OU and that the base DN is correct, according to where you placed the user account. (Needless to say that the user must have next to no privileges at all)

Set up the User model class

The next step is our user model. This should be like any model, you have except that it will be based on the $ldap database config, its primary key will be the LDAP dn attribute and that it will need no table.

class User extends AppModel {
    var $name = 'User';
    var $useDbConfig = 'ldap';

    var $primaryKey = 'dn';
    var $useTable = '';

    /**
     * return true if the userName is a member of the groupName
     * Active Directory group
     */
    function isMemberOf($userName, $groupName)
    {
        // trivial check for valid names
        if (empty($userName) || empty($groupName))
            return false;

        // locate the user record
        $userData = $this->find('first',
                                array(
                                    'conditions' => array(
                                        'samaccountname' => $userName
                                    )
                                )
                            );
        // no user by that name exists
        if (empty($userData))
            return false;

        // check if the userin question belongs to any groups
        if (!isset($userData['User']['memberof']))
            return false;

        // search all groups that our user if a meber
        $groups = $userData['User']['memberof'];
        foreach( $groups as $index => $group)
            if (strpos( $group, $groupName) != false)
                return true;

        return false;
    }
}

We have also added a member function which allows us to determine if a user-name belongs to an Active directory group that will be very handy when checking for application privileges later on.

Set up the LdapAuth Component

And finally the "auth" part. The standard CakePHP Auth component will not work for us so we need a specialized version that thanks to analogthemes again is accessible from his web site through here. Just place ldap_auth.php to your APP/controllers/components/ directory and change your AppController so that it uses LdapAuth.

This however is not enough. There is one little thing that needs to be changed in the LdapAuth code, so that authenticating in AD works: Locate the login() function in line 153 of ldao_auth.php and change line 156 so that it looks like this:

     ...

     function login($uid, $password) {
        $this->__setDefaults();
        $this->_loggedIn = false;
        // $dn = $this->getDn('uid', $uid);
        $dn = $this->getDn('samaccountname', $uid);
        $loginResult = $this->ldapauth($dn, $password); 
        ...

My simple approach to security says that an application has two types of users. Readers (i.e. everyone) and writers (admins only). We set the administrative users to be the members of a specific group in the active directory server. That way our application controller looks like this :

class AppController extends Controller {
    const ADMIN_KEY  = 'myApp-Admin';
    const AUTH_GROUP = 'MyApp-Admins';

    var $components = array('RequestHandler', 'LdapAuth', 'Session');
    var $helpers = array(
                        'Form', 'Html', 'Locale', 'Session',
                        'Js' => array('Jquery')
                    );
    var $isAdmin;

    function beforeFilter()
    {
        // pr($this->referer());
        $this->isAdmin = false;
        $this->LdapAuth->authorize = 'controller';
        $this->LdapAuth->allowedActions = array(
                                            'index',
                                            'view',
                                            'details'
                                        );

        $this->LdapAuth->loginError = "Invalid credentials";
        $this->LdapAuth->authError  = "Not authorized!";

        $userInfo = $this->LdapAuth->user();

        if (!empty($userInfo)) {
            $user = $userInfo['User'];
            // setup display username and user's full name
            $this->set('loggedInUser', $user['samaccountname']);
            $this->set('loggedInFullName', $user['cn']);

            // if we already have the admin role stored in the session
            if ($this->Session->check(self::ADMIN_KEY))
                $this->isAdmin = $this->Session->read(self::ADMIN_KEY);
            else {
                // determine the admin role from wheither the current 
                // user is a member of the AUTH group
                $this->LoadModel('User');
                $this->isAdmin = $this->User->isMemberOf(
                                                $user['samaccountname'],
                                                self::AUTH_GROUP);
                $this->Session->write(self::ADMIN_KEY, $this->isAdmin);
            }
        } else {
            $this->Session->delete(self::ADMIN_KEY);
        }

        $this->set('admin', $this->isAdmin);
    }

    function isAuthorized()
    {
        // Only administrators have access to the CUD actions
        if ($this->action == 'delete' ||
            $this->action == 'edit' ||
            $this->action == 'add')
          return $this->isAdmin;

        return true;                
    }
}

That way, everyone can access our view, details and index actions of all the application controllers, but will need to enter the user name and password of an ADMIN_GROUP member to gain access to the rest of our controller methods. This tutorial however is has gone long enough. To finish it, please remember to create your user controller and you login user view ...

Thursday, 2 June 2011

C and UTF-8 data in Linux

During the last few days, I found myself struggling to create a PHP extension what would allow PHP to convert common characters between Greek and Latin, so that typed codes would for instance, always use the same version of 'A' no matter the language that the user keyboard is switched to. You see whether you type a Latin 'A' (U+0041) or a Greek 'Α' (U+0391) the two letters appear to be the same despite the difference in their internal representation. The same applies to other common letters like 'E', 'P' and 'Y'

I reckoned that this conversion and translation of characters based on their position on some given string would be an excellent chance to remember string pointers from my student C days, so I set out to create a library with functions called latinToGreek() and greekToLatin() that would do the conversion and return the new string.

Like most people in similar positions, I tried googling for C and Unicode howtos and run into some very good articles but nothing was in the form of a recipe that I could follow. I did read many of them and managed to get the work done. The basic idea here is that since UTF-8 uses a variable number of bytes per character, we need to convert UTF-8 strings into wchat_t strings, then process them like they were ordinary constant length null terminated character arrays and finally convert them back to UTF-8 single byte character strings in order for them to display correctly.

Following is the check list of things to do before actually playing with UTF-8 encoded Unicode.

  1. Use setlocale to set the current program locale to something that supports UTF-8. For example 'en_US.UTF-8' is just fine.
    Bear in mind, that the default locale for C programs in the "C" locale and unless you set the locale correctly nothing will work as expected.
  2. Read your input using normal char[] arrays. Just make sure that hey are big enough. Remember that UTF-8, uses one, two or even more bytes per individual character, so a logical guess would be to allocate an array at least twice the size of your maximum anticipated input.
    Do not forget that good old strlen() will still give you the size of your input in bytes, but not in characters.
  3. Convert multibyte input to wchar_t[] input in order to perform any processing. Remember that wchar_t constants need to be prefixed by an L, so '\0' is now L'\0'.
    Functions mbstowcs and wcstombs can be used to perform the conversion to and from.
  4. Perform your processing as you would ordinarily do using wchat_t data and wchar_t speciffic functions. For example toupper() for wchar_t is now towupper().
  5. Convert your data back to multibyte format using wcstombs and return them to the user.

... and that's about it.

Tuesday, 17 May 2011

CakePHP and FreeTDS on CentOS 5

We are almost finished with a project using CakePHP 1.3.8 running on a CentOS 5 box that accesses a Microsoft SQL Server 2008 database, using PHP 5.3.6-4 and the freetds driver, all available from Remi's repository.

This is a report of all the little problems we faced and the solutions we came up with :

  1. The first thing that hit us, was he encoding of the saved data. Although we used NVARCHAR columns for all character fields, we ended up seeing gibberish inside the SQL Server Management Studio, whenever we had to enter anything that was not Latin. Both the server and the application are using UTF-8 locales and data entered via Cake would be returned correctly, however there was no way to get the MSSQL Management Studio to display UTF-8 data.
    After some dispute about whether or not to leave things as they are we decided to utilize a behavior that converted data to and from different character sets before storing and retrieving them from the database. The relevant post from back 2009 is accessible via the following link.
    This approach kept almost everyone happy.
  2. Second issue was the length of the Model's visual fields. I have blogged about this a few days ago and it turns out that the maximum number of characters allowed to a virtual field name is 14.
  3. And finally the stored procedure issue. Up until now, we have been unable to properly call a T-SQL stored procedure using mssql_init() and mssql-bind(). The only workarround for doing the job is to use the mssql_query() function and execute an "EXEC ... " query.
    The code we use -- as a Model method -- is more or less like this :
        /**
         * Execute the T-SQL stored procedure named "dbo.storedProcName"
         * and return execution status.
         *
         * NOTE: if the stored proc uses the T-SQL RAISERROR statement
         *       to trigger some sort of error condition, then
         *       mssql_query still returns true but the 
         *       mssql_get_last_message() will contain the actual error
         *       text that will be returned to the caller.
         */
        function callStoredProc($procParam, &$errorMessage = '')
        {
            // get the low level database connection from the model data
            $conn = $this->getDataSource()->connection;

            // execute a query that calls the stored proc
            $sqlResult = mssql_query(
                          " EXEC dbo.storedProcName
                            @procParam = $procParam;",
                          $conn
                        );

            $errorMessage = mssql_get_last_message();
                
            if ($sqlResult == false) {
                // something went wrong 
                $result = false;              
            } else {                    
                // stay on the safe side            
                if (is_resource($sqlResult))
                    mssql_free_result($sqlResult);
                // if the stored procedure error message is empty then all
                // is well
                $result = $errorMessage == '' ? true : false;
            }

            // return success
            return $result;
        }

As always, I try to update the list if anything new comes up.

Tuesday, 3 May 2011

CakePHP: Dynamicaly disabling links

Sometimes we get buried so deeply into ... nuclear science that we forget that after all CakePHP is just PHP, the resulting pages are just HTML and you don't need JQuery to disable a link. Setting it's onlick event to return false, usually does it.

The problem was simple: Given a list of records, (arrivals) selectively enable an action link for each record, (create a purchase order in our case). based on a record attribute (that is whether the purchase order has already been created).

Now, that is now so hard. Well, if you already have something like :

<?php echo $this->Html->link(
                    $this->Html->image('actions/upload.png'),
                    array(
                          'action' => 'createPurchasOorder',
                          $arrival['Arrival']['id']
                    ),
                    array(
                      'escape' => false,
                      'title' => 'Create purchase order for the arrival.'
                    )
                ); ?> 

inside a PHP foreach loop, then it's not that difficult to transform it into :

<?php 
         $canCreatePO = empty($arrival['Arrival']['purchase_order_code']);
         echo $this->Html->link(
                    $this->Html->image('actions/upload.png'),
                    array(
                          'action' => 'createPurchasOorder',
                          $arrival['Arrival']['id']
                    ),
                    array(
                      'escape' => false,
                      'onclick' => $canCreatePO 
                              ? 'return true;'
                              : 'return false',
                      'title' => $canCreatePO 
                              ? 'Create purchase order for the arrival.'
                              : 'Purchase order has already been created.'
                    )
                ); ?> 

So, why the hell did it take me an entire morning to figure this out....

Thursday, 21 April 2011

CakePHP: Length of a virtual field's name

Perhaps the fact that I am accessing an MS SQL 2008 server from Linux using the freetds driver could be the cause of my troubles.

What I did find out today, is that when using the $virualFields property of a model the size of the virtual field's name, cannot exceed 14 characters.

My setup here is: CakePHP 1.3.8, running on CentOS 5.6 x86_64 using PHP 5.3.6-3 from Remi's repository.

And here is my case: My Model looks pretty much like this

class ArrivalPackage extends AppModel {
 var $name = 'ArrivalPackage';
 var $belongsTo = array('Arrival', 'Product', 'PackageType');
 var $hasMany = 'PackageLine';

 var $virtualFields = array(
   'reserved_date_gr' => 'CONVERT(NVARCHAR(10), ArrivalPackage.reserved_date, 105)',  //  not good 
   'loaded_date_gr'   => 'CONVERT(NVARCHAR(10), ArrivalPackage.loaded_date, 105)',
   'sold_date_gr'     => 'CONVERT(NVARCHAR(10), ArrivalPackage.sold_date, 105)'
 );
}

After I baked a view and controller for PackageLines I added a pr($packageLine); on the top of my view.ctp view file. The result was :

Array
(
    [ArrivalPackage] => Array
        (
            [id] => 3
            [arrival_id] => 3
            [product_id] => C2ΕLΙLΤ075000000075DULW4
            [supplier_package_number] => 345345345
            [shelman_package_number] => 1099228
            [SSCC] => 
            [min_length] => 2600
            [max_length] => 2600
            [package_type_id] => 1
            [expected_m3] => 0
            [total_pcs] => 20
            [total_m3] => 0.015
            [total_m] => 2
            [loaded] => 0
            [sold] => 1
            [reserved] => 1
            [loaded_date] => 
            [sold_date] => 2011-04-21
            [reserved_date] => 2011-04-18
            [reserved_user] =>  
            [loaded_date_gr] => 
            [sold_date_gr] => 21-04-2011
        )
    
    [0] => Array
        (
            [ArrivalPackage__reserved_date_] => 18-04-2011
        )
    
)

Only after I shortened the name of the reserved_date_gr field to rsvd_date_gr, did I get my expected

Array
(
    [ArrivalPackage] => Array
        (
            [id] => 3
            [arrival_id] => 3
            [product_id] => C2ΕLΙLΤ075000000075DULW4
            [supplier_package_number] => 345345345
            [shelman_package_number] => 1099228
            [SSCC] => 
            [min_length] => 2600
            [max_length] => 2600
            [package_type_id] => 1
            [expected_m3] => 0
            [total_pcs] => 20
            [total_m3] => 0.015
            [total_m] => 2
            [loaded] => 0
            [sold] => 1
            [reserved] => 1
            [loaded_date] => 
            [sold_date] => 2011-04-21
            [reserved_date] => 2011-04-18
            [reserved_user] =>  
            [resvd_date_gr] => 18-04-2011
            [loaded_date_gr] => 
            [sold_date_gr] => 21-04-2011
        )
)

Funny thing is that the actual sql statement produced by Cake, no matter what you name the field is valid for SQL Server, still unless the field name becomes smaller than 14 characters things don't work as you would expect them.

Tuesday, 19 April 2011

CakePHP: Storing paging info in the Session

Warning: This post applies to CakePHP 1.3. If you are using CakePHP 2, then visit the updated version available through here.

There is no way you can miss it. Sooner or later you 'll end up having baked your controllers and views only to discover that when you edit a record from the 4th page and press submit on the edit page, much to your annoyance you end up on page one. Needless to say that whatever sort order was there is now gone with the wind.

I have googled a bit and found out the following article in Stack Overflow. The solutions provided over there seemed rather complicated to me, so I decided to craft my own.

Now here is the deal: From what I have seen in cake 1.3, paging is controlled by the value of three parameters: page, sort and direction. All we need to do is provide a way to store them in the session and then have the controller's redirect method inject these back into the URL parameters when they are not present.

<?php
     
class AppController extends Controller {
    const SESSION_KEY = 'PagingInfo.%s.%s';
        
    public $components = array('Session');            

    protected function savePagingInfo()
    {
        // create an array to hold the paging info
        $argsToSave = array();

        // remove paging info from passed args
        foreach( $this->passedArgs as $key => $value)
            if ($key == 'page' || $key == 'sort' || $key == 'direction')
                $argsToSave[$key] = $value;

        // abort if any paging info was found
        $sessionKey = $this->buildSessionKey();
        if (!empty($argsToSave)) {
            $this->Session->write( $sessionKey, $argsToSave);
            return;
        }
        
        // no paging info let's see if we have something in the sesion
        if ($this->Session->check($sessionKey)) {
                $pagingInfo = $this->Session->read($sessionKey);
                $this->passedArgs = array_merge( $this->passedArgs, $pagingInfo);
        }
    }
                        
    private function buildSessionKey($action = NULL)
    {
        if (empty($action))
            $action = $this->action;
            
        return sprintf( self::SESSION_KEY, $this->name, $action);
    }
}
     
?>    

This approach may be simplistic but in my case it worked. All I had to was call $this->savePagingInfo(); from my index() action and then let the rest of the baked code do its work.

Note

And one last comment, there are many cases that you need paging during display of master detail record sets. In cases like these savePagingInfo() will save paging status for the detail records matching the current master and when the master changes, the paging in the session becomes invalid. My simple solution to this is to add a 'page' => 1 entry in the $url params pointing to a master record change. With the design of the application I am currently working on this proves just good enough.

Wednesday, 1 December 2010

A simple GUID generator function for Delphi

Here is a little code sample, that I run into the other day. The actual URL is here, but since the original site is now closed, I thought that I could save this posting on my blog as well.

If anybody running CodeGear RAD Studio 2007 ever wishes to play with GUID's then this function is a real must have, All credit goes to the original poster.

interface

function GUIDString: String;

implementation

uses ActiveX;
  
function GUIDString: String;
var
  Guid: TGUID;
  wsGuid: WideString;
begin
  Result := '';
  { create the 128 bit integer }
  if CoCreateGuid(Guid) = S_OK then begin
    { create a buffer of suitable size }
    SetLength(wsGuid, 80);
    { convert the 128 bit integer into a Unicode string }
    if StringFromGUID2(Guid, POleStr(wsGuid), 80) > 0 then
      { convert the Unicode to a String for output }
      Result := OleStrToString(PWideChar(wsGuid));
    end;  { if CoCreateGuid ... }
end;

Sunday, 14 November 2010

Packages required to run skype on Fedora 15,16,17

Just installed skype 2.2 on my Fedora 15 (x86_64) box and it works. The actual rpm can be downloaded from here. The rpm installs itself without checking for dependencies, so in order to run skype correctly you need the 32bit versions of the qt and alsa libraries. The actual command to install these packages is:


sudo yum -y install qt.i686 qt-x11.i686 libXScrnSaver.i686 alsa-lib.i686

... and away we go.

Update (2012-06-09). Verfied for fedora 17

Thursday, 19 August 2010

Friday, 9 July 2010

Hello T-SQL and ASP.NET

Half of me wishes things had gone the way I planned, so now we would be programming with CakePHP storing data in Oracle and calling remote enabled function modules in order to communicate with SAP. Then my other half says: "Ok, no more reading and mangling with ABAP spaghetti code written by somebody afraid to declare a single local variable and trying to figure out what all these magic tables with the enlightening four-letter names do!" Of course getting rid of ABAP is the bright side. The dark side said says "Hello from 1 Microsoft Way Redmond WA"

After the company I work for was sold out right before Christmas, the new management decided that SAP was too expensive and too complicated so from now the entire group we will be using a Greek ERP made by Entersoft SA. The new ERP is an N-Tier system running under Windows written in .NET and uses Microsoft SQL Server as data store. So what is left for us now is to learn and program in .NET

Being the Linux junkie I am, this development in my developer status triggered various comments from friends and neighbours. The most interesting was this video trailer which much like Sgt. Pepper's Lonely Hearts Club Band is ... guaranteed to raise a smile.

So Java vs .NET; the choice is not always yours sο sit back and enjoy

Stay tuned for more.

Wednesday, 14 April 2010

Flash player for 64 bit Linux

Thanks to Leigh's repository for Fedora, I almost forgot where to find the latest version of flash player.

So, to save a little bit of googling ...

Last update : August 11, 2011

Sunday, 7 February 2010

Linux: Mass rename of files

When people give me files, especially images, coming from windows machines I always have problems with capitalised extensions. JPEG format files for instance appear as image.JPG instead of image.jpg. Worst case is that in some collections some file extensions appear capitalised while others in lower case.

I have searched the web for some simple solution to this problem and ended up seeing sed commands and pipes that UNIX gurus are so fond off,

So, if you wanted to rename all the .php3 to .php, this would be the command:
ls -d *.php3 | sed 's/\(.*\).php3$/mv "&" "\1.php"/' | sh

As Fortune keeps reminding me, there is more than one way to skin a cat, so a more complete discussion on mass renaming with Linux can be found on Gareth Anderson's GNU/Linux Command-Line Tools Summary book article directly accessible from here.

Anyway, after a little bit of googling, it turned out to my surprise that the simplest way to do this was inside my own Fedora distro. The magic command is rename and the man entry is small, simple and accurate :

RENAME(1)                  Linux Programmer’s Manual                 RENAME(1)

NAME
       rename - Rename files

SYNOPSIS
       rename from to file...
       rename -V

DESCRIPTION
       rename will rename the specified files by replacing the first occurrence of from in their name by to.

       -V, --version
              Display version information and exit.

       For example, given the files
              foo1, ..., foo9, foo10, ..., foo278, the commands

              rename foo foo0 foo?
              rename foo foo0 foo??

       will turn them into foo001, ..., foo009, foo010, ..., foo278.

       And
              rename .htm .html *.htm

       will fix the extension of your html files.

SEE ALSO
       mmv(1), mv(1)

AVAILABILITY
       The   rename   command   is   part   of   the   util-linux-ng   package   and   is   available  from  ftp://ftp.ker-
       nel.org/pub/linux/utils/util-linux-ng/.

                                1 January 2000                       RENAME(1)

The rename command is also available on all EL clones like (CentOS and Oracle EL).

Wednesday, 20 January 2010

CakePHP: Calling Oracle stored procedures and functions

Our testing with Cake is almost complete. We have been able to read and write data correctly in our non UTF-8 Oracle database, managed to communicate with SAP, played enough with AJAX, so our users will not complain when dealing with tree like data and even managed to get authenticated by our MS-Windows 2003 active directory servers. So the last question left was how does one call an Oracle PL/SQL stored procedure or function from CakePHP ?

A little bit of googling and a little bit of digging into the CakePHP code revealed the following: The most common approach to calling stored procedures and functions is to create a member function in your model class and set up the call from there. In the simple case of calling a stored procedure with IN parameters only, the Model's query() method can be used to perform the actual call via the CALL PROC_NAME( ... ) SQL statement. The usual approach is to create a model method like this :

class MyModel extends AppModel {
    var $name = "MyModel";

    ...
    function callStoredProc( $param1, $param2)
    {
        $this->query("CALL my_stored_proc( $param1, $param2");
    }
    ...
}

If however you need to get data in and out of Oracle then you have to get your hands dirty and set up the call using low level oci_* functions. A simple example will clarify everything.

Let us suppose that you library users require that you display the average number of pages of the books stored in your library. A simple PL/SQL function to return this would probably be something like this: /p>

CREATE OR REPLACE FUNCTION average_book_pages RETURN NUMBER
IS
   page_avg NUMBER;   
BEGIN
   SELECT avg( num_pages) 
      INTO page_avg
      FROM books;
   RETURN page_avg;   
END AVERAGE_BOOK_PAGES;

The next thing to do would be to create a getAvergaeBookPages() function in our Book model class :

<?php
class Book extends AppModel {
    var $name = 'Book';
    var $belongsTo = ...
    var $validate = array( ...

    function getAverageBookPages()
    {
        // every model has a datasource
        $dataSource = $this->getDataSource();
        // and every datasource has a  database connection handle
        $connection = $dataSource->connection;
        if (!$connection)
            return -1;

        // from now you need this Oracle specific approach 
        $statement = oci_parse( $connection,
                "begin :ret := AVERAGE_BOOK_PAGES(); end;");
        oci_bind_by_name($statement, ':ret', $r, 100);
        oci_execute($statement);
        return $r;
    }
}
?>

The last parameter of oci_bind_by_name is the number of bytes to allocate for returning the string representation of the bind variable. Just make sure that you allocate enough memory for that. My test data yield an average of 762,666666666666666666666666666666666667 pages per book (Thank you Mr. Minasi) and so oci_execute kept giving me ORA-06502: PL/SQL: numeric or value error: character string buffer too small until I raised the value to 100.

So that does it. Now calling this from you controller code is as easy as :

<?php
class BooksController extends AppController {
    var $name = 'Books';

    ...

    function index()
    {
        $this->Book->recursive = 0;
        $this->set('books', $this->paginate());
        $this->set('averagePages', $this->Book->getAverageBookPages());
    }

    ...
}

Thursday, 7 January 2010

CakePHP: The dependent listboxes problem

Note: This example is outdated. I have a newer blog post that addresses the same issue using more up to date methods and functionality.

When creating dynamic web-sites, sooner or later you are going to face the problem of providing dependent list boxes. Imagine the case where one wishes to select a city from a list grouped by region, or a user from a group, an invoice from an order etc. Grouping things into categories is very common in real life and as far as I am concerned, it is almost always a must for your application to be able to utilize such groupings.
In CakePHP -- as far as I know -- there are two ways you can help your users pick up a value from a list of grouped items. One is to create a select box whose items are organized in selection groups sorted in some logical way. the other way in to use dependent AJAX triggered combo or list boxes where selection on the first will filter the items displayed on the second.
In this posting I will provide code that handles both cases.

The sample data

As an exercise for learning Cake, I developed a small application that manages the IT department books. Each Book belongs to a BookCategory and each BookCategory belongs to a BookCategoryGroup. Reversely, each BookCategoryGroup has many BookCategory and each BookCategory has many Book. The corresponding tables have foreign keys adhering to the CakePHP conventions, so I am not going to waste any more time explaining the data structure.
The goal here is to help our users, when adding or editing book records, to find the right category for each book given the organization of book categories in book category groups. Like I said in the introduction there are two ways we can accomplish this

One combo box organized in selection groups

The way is very easy to implement and may become particularly handy whenever the total number of list items is relatively small. Cake's Form::imput method will create option groups if the array containing the options for a select box is organized into sub arrays so if we add the following function in our BooksController ....
    private function prepareCategoriesCombo()
    {
        // gain access to the BookCategoryGroups model class
        $this->loadModel('BookCategoryGroup');
        // prepare a list of all book category groups
        $this->BookCategoryGroup->recursive = 0;
        $bookCategoryGroups = $this->BookCategoryGroup->find(
                                'all',
                                array(
                                    'conditions' => array(),
                                    'order' => array('BookCategoryGroup.name')
                                )
                            );

        // create an empty array to hold the combo box options
        $bookCategories = array();
        foreach( $bookCategoryGroups as $bookCategoryGroup) {
            $groupId = $bookCategoryGroup['BookCategoryGroup']['id'];
            $groupName = $bookCategoryGroup['BookCategoryGroup']['name'];
            // create a sub array for each group category
            $bookCategories[] = $groupName;
            // fill the array with the categories corresponding to the group
            $bookCategories[$groupName] = $this->Book->BookCategory->find(
                                'list',
                                array (
                                    'conditions' => array(
                                        'BookCategory.book_category_group_id' => $groupId
                                        ),
                                    'order' => array(
                                        'BookCategory.name'
                                        )
                                )
                                    );
        }
        return $bookCategories;
    }
Supposing that you have baked your original controller and view code with the cake script, your add() or edit() controller actions need to have the following in order to use the option grouped combo:
   ...
   $bookCategories = $this->prepareCategoriesCombo();
   ...
while the view template will require no change at all (i.e. a simple echo $form->input('book_category_id'); will suffice). As I said earlier on, this method is simple enough and unless you intent to let your users pick a US zip code organized by state, this may be the preferred solution for many cases. If however you have lots of data and an untamable desire for ajax, read on; fear not however CakePHP's approach to AJAX makes this look also like a piece of cake.

The AJAX way: Two combos with one auto filtering the other

The basic idea behind AJAX is the following: You start by defining an area in you web page identifiable via the the HTML id attribute. Then when the user clicks on a button or changes the value of some control (edit, list or combo), you make an asynchronous call to the web server -- that is without having to reload the page -- and the server returns HTML code that is ready to be placed inside that area. The actual way you implement this depends on the libraries and the AJAX framework you use. Cake does that using the prototype and the Scriptaculus frameworks.
So to get things started, download prototype and scriptaculus and place the following files in your APP/webroot/js folder:
builder.js   dragdrop.js  prototype.js      slider.js  unittest.js
controls.js  effects.js   scriptaculous.js  sound.js
Having done that, modify you application layout in order to include them. Open APP/view/layouts/default.ctp and change the HTML head part so it looks like this :
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>
      <?php echo $title_for_layout; ?>
    </title>
    <?php echo $html->css('it-library'); ?>
    <meta name="Generator" content="Quanta Plus" />
    <meta name="Author" content="Thanassis Bakalidis" />
    <?php if (isset($javascript)) : ?>
      <?php echo $javascript->link('prototype.js'); ?>
      <?php echo $javascript->link('scriptaculous.js'); ?>;
    <?php endif; ?>
    <?php echo $scripts_for_layout; ?>
  </head>
The next thing to do is to modify our controller in order to provide Javascript and AJAX support: Our books controller now looks like the following:
class BooksController extends AppController {

    var $name = 'Books';
    var $helpers = array('Html', 'Form', 'Javascript', 'Ajax');
    var $components = array('RequestHandler');

    ...
}
Next go to your view template -- may that be the add or edit.ctp -- and change the initial echo $form->input('Book.book_category_id'); line in order to look like the following:
        // here are the two list boxes displaying groups and categories
        // aim is to create an auto-filter effect with AJAX
        echo $form->label( 'BookCategory.book_category_group_id',
                            'Category Group');
        echo $form->select('BookCategory.book_category_group_id',
                            $bookCategoryGroups,
                            $bookCategoryGroupId,
                            array(
                                'id' => 'bookCategoryGroups'
                            ),
                            FALSE);
        echo $form->input('Book.book_category_id', array('id' => 'bookCategories' ));

        // each time the bookCategoryGroups element changes we are to
        // asynchronously call the updateSelect action of the current 
        // controller and insert whatever the action produces inside the 
        // html DOM element identified by bookCategories
        $ajaxOptions = array('url' => 'updateSelect','update' => 'bookCategories');
        echo $ajax->observeField('bookCategoryGroups',$ajaxOptions);
I believe that the code is self explanatory. Now let us add the updateSelect method of the BooksController class
    function updateSelect()
    {
        $groupId = $this->data['BookCategory']['book_category_group_id'];
        if (!empty( $groupId )) {
            $options = $this->getBookCategoriesForGroup( $groupId);
            // these are the combo box options to be used in the view file
            $this->set('options',$options);
        }
    }
There is one thing to mention here: the AJAX code produced by observeField() serializes the entire field that is supposed to observe, so this will be available in the controller action as $this->data['Model']['field'].
Next we need to create the actual view code. Create a file named update_select.ctp inside your APP/views/books directory and place the following code inside (Thanks HerbCSO):
<?php
    // create  tags coming from a $options variable
    // This is to be used by AJAX in order to fill the contents of a combo
    // box
    if(!empty($options)) {
        foreach($options as $key => $value) {
             echo "<option value=\"$key\">$value</option>";
        }
    }
?>
Now we have everything in place. The only thing left to do is to initialize the two combo boxes so that they contain the correct data, i.e. all the group categories for the top combo and the categories for the selected records category group on the second, during initial page load. To achieve this I have created two additional functions in the BooksController class:
    private function getBookCategoriesGroups()
    {
        $this->loadModel('BookCategoryGroup');
        $this->BookCategoryGroup->recursive = 0;
        return $this->BookCategoryGroup->find('list',
                                                array(
                                                    'conditions' => array(),
                                                    'order' => array(
                                                        'BookCategoryGroup.name'
                                                        )
                                                )
                                            );
    }

    private function getBookCategoriesForGroup( $groupId)
    {
        return $this->Book->BookCategory->find('list',
                                array(
                                    'conditions' => array (
                                        'book_category_group_id' => $groupId
                                        ),
                                    'order' => array(
                                            'BookCategory.name'
                                        )
                                )
                            );
    }
Now my controller's edit action -- which as I mentioned earlier, was baked by cake -- looks like the following.
    function edit($id = null)
    {
        if (!$id && empty($this->data)) {
            $this->Session->setFlash(__('Invalid Book', true));
            $this->redirect(array('action'=>'index'));
        }
        if (!empty($this->data)) {
            if ($this->Book->save($this->data)) {
                $this->Session->setFlash(__('The Book has been saved', true));
                $this->redirect(array('action'=>'index'));
            } else {
                $this->Session->setFlash(__('The Book could not be saved. Please, try again.', true));
            }
        }

        if (empty($this->data)) {
            $this->data = $this->Book->read(null, $id);
        }

        // set up additional book record parameters
        $sites = $this->Book->Site->find('list');
        $bookTypes = $this->Book->BookType->find('list');
        $languages = $this->Book->Language->find('list');

        // setup the two AJAX operated combo boxes
        $bookCategoryGroups = $this->getBookCategoriesGroups();                
        $bookCategoryGroupId = $this->data['BookCategory']['book_category_group_id'];
        $bookCategories = $this->getBookCategoriesForGroup( $bookCategoryGroupId);

        $ratings = $this->Book->Rating->find('list');
        $publishers = $this->Book->Publisher->find('list');

        $this->set( compact( 'sites','bookTypes','languages',
                             'bookCategories', 'bookCategoryGroups', 'bookCategoryGroupId',
                             'ratings','publishers'));
    }
Needless to say that when adding a record, the initial $bookCategoryGroupId can be set to an initial value say 1 and then let your uses change to whatever seems appropriate.
I have tested this with CakePHP 1.2.5 on both Firefox (versions 3 and 3.5) and IE (version 8).
As a last statement, I would like to point out that I am by no means an expert on AJAX or CakePHP. I got my info from an earlier posting by DEVMOZ and the CakePHP AJAXHelper class info page. I have put this down as a working reference to a real problem, that anybody can copy -- hopefully -- easy to modify code.

Wednesday, 16 December 2009

CakePHP and Oracle on CentOS. My own how to guide

During the last couple of days I formated and set up from scratch my CakePHP development server, using CentOS, php 5.3 from Remi and the Oracle 11gR2 clients. The log of my actions in PDF can be downloaded from here.

During the following days I will setup one more server -- supposed to be the one that we will use productively -- following these same instructions. If I find any mistakes I will correct them and post back the orginal file here.

Update history

  • Dec-28-2009: Added information about setting up and running the cake scripts from the command line.
  • Jan-21-2010: Verified contents on a new installation and added reminder for configuring the firewall.

Tuesday, 15 December 2009

CakePHP: A behavior for acessing non UTF Oracle databases

The origins of our company's Oracle database date back at the beginning of the decade. At that time we had Oracle version 9i running on SuSE Linux 8.2 and the expected thing to do back then was to create a database using the EL8ISO8859P7 character set. After eight years we are still using Oracle. Now the database is 11g and the database server is OEL 5.4. Basic data structures however, are still the same as they were back in 2002.

During the evaluation of CakePHP as our next development environment we very soon run into the problem of trying to insert and retrieve Unicode data from a non-unicode database. Since the encoding key of $defualt array member of the DATABASE_CONFIG class (stored in app/config/database.php file) has no effect when connecting to Oracle databases, we ended up creating an additional translation layer, that would convert data to and from Unicode when reading from and writing to Oracle.

CakePHP's way of doing this kind of staff is to create behaviors. Ours is called CharsetConverter, so by CakePHP's standards it is implemented in a class named CharsetConverterBehavior that is stored in a file named charset_converter.php which is located in the APP/models/behaviors directory.

The approach here uses the mb_convert_encoding function provided with the php-mbstring package. The code implementation is the following

<?php
/**
 * A simple behavior that allows cake PHP to use single byte Oracle
 * and possibly other vendor -- databases
 * Tested with Oracle 11gR1
 *
 * @version 0.1
 * @author Thanassis Bakalidis
 */
class CharsetConverterBehavior extends ModelBehavior {
    // we have an Oracle database that dates back to 2002 so
    const DEFAULT_DB_LOCALE = 'ISO-8859-7';
    const DEFAULT_PAGE_LOCALE = 'UTF-8';

    const READING_FROM_DB = TRUE;
    const WRITING_TO_DB = FALSE;

    var $databaseLocale;
    var $webpageLocale;
    var $Model;

    function setup(&$model, $settings=array())
    {
        $this->Model = $model;

        $this->databaseLocale = isset($settings['databaseLocale']) ?
                                    $settings['databaseLocale'] :
                                    self::DEFAULT_DB_LOCALE;
        $this->webpageLocale = isset($settings['webpageLocale']) ?
                                    $settings['webpageLocale'] :
                                    self::DEFAULT_PAGE_LOCALE;
    }

    /**
     * Change the query where clause to the datbase native character set.
     */
    function beforeFind( &$queryData, $queryParams)
    {
        if (!isset( $queryParams['conditions']))
            return $queryParams;

        $queryParams['conditions'] = $this->recodeRecordArray(
                                                        $queryParams['conditions'],
                                                        self::WRITING_TO_DB);
        return $queryParams;
    }

    /**
     * Convert fetched data from single byte to utf-8
     */
    function afterFind(&$model, $results, $primary)
    {
        return $this->recodeRecordArray($results, self::READING_FROM_DB);
    }

    /**
     * Convert data to be saved into the database speciffic locale
     */
    function beforeSave()
    {
        $this->Model->data = $this->recodeRecordArray( $this->Model->data,
                                                       self::WRITING_TO_DB);
        return true;
    }

    /**
     * Recursively traverse and convert the encoding of the array passed
     * as parameter.
     */
    function recodeRecordArray(&$recordArray, $loading = TRUE)
    {
        foreach( $recordArray as $key => $value)
            if (is_array($value))
                $recordArray[$key] = $this->recodeRecordArray($value, $loading);
            else {
                if (is_numeric($value))
                    continue;
                $recordArray[$key] = $loading ?
                                    mb_convert_encoding(
                                                $value,
                                                $this->webpageLocale,
                                                $this->databaseLocale)
                                    :
                                    mb_convert_encoding(
                                                $value,
                                                $this->databaseLocale,
                                                $this->webpageLocale);
            }
        return $recordArray;
    }
}
?>

Once we have this in place, using the new behavior in one of our models is as simple as setting the correct value of the $actAs variable. Here is a simple example of a model using the Character set convention and validation.

<?php
    class Task extends AppModel {
        var $name = 'Task';
        var $actsAs = array(
                    'CharsetConverter' => array(
                                            'databaseLocale' => 'ISO-8859-7',
                                            'webpageLocale' => 'UTF-8'
                                        )
                    );
        var $validate = array(
                    'title' => array(
                                'rule' => 'notEmpty',
                                'message' => 'Task title cannot be left blank'
                    )
                );
    }
?>

Almost all applications contain more than one model. Perhaps the best place to put the $actAs definition would be the AppModel class defined in the file app_model.php in the root of your app directory

I also understand that writing a behavior to accomplish the job of the database driver is not the best solution. Since I have nothing better for the moment, I guess I will have to start every new CakePHP project by first changing my app_model.php file.

Tuesday, 1 December 2009

How to set up the SAPRFC extension for PHP on CentOS 5

SAPRFC

SAPRFC is an extension module for PHP 4 and 5, that makes it possible to call ABAP function modules running on a SAP R/3 system from PHP scripts.

I always wanted to test how SAP's remote function calls work together with PHP and since I am already evaluating CakePHP as our next development platform, I decided that the occasion was right to give it a try.

Next thing I did was to get my hands on The SAP Developer's Guide to PHP book by Craig S. Cmehil, which is an $85 cost and 98 page long (indexes included) tutorial on how to set up and use SAPRFC for PHP. Unfortunately the second chapter that discusses setting up your development system focuses mainly on Windows, so this post will contain the steps I took to set up SAPRFC for PHP on my x86_64 CentOS 5.4 server.

Package Requirements and Downloads

To get the latest PHP packages for Enterprise Linux I have used Remi's repository.

# wget http://download.fedora.redhat.com/pub/epel/5/x86_64/epel-release-5-3.noarch.rpm
# wget http://rpms.famillecollet.com/enterprise/remi-release-5.rpm
# rpm -Uvh remi-release-5*.rpm epel-release-5*.rpm

Remi's packages depend on the EPEL repository, so I am posting the installation procedure for EPEL as well. (if you haven't installed it yet, now will be a good time to do so.)

In addition to any other PHP packages that your application requires, in order for the SAPRFC to compile correctly, you will also require the php-devel package.

Next thing is the SAPRFC package itself. The method to install it will be to build the saprfc extension as a dynamic module without rebuilding PHP. (Remi has already done that for us.) The package itself can be downloaded from here.

Continue by installing SAP's Non Unicode RFC SDK version 6.20 or 6.40. This must be downloaded directly from the SAP Service Support Portal. You will need a customer SAP support ID

Be advised however, that SAP has implemented some form of a DNS transparent cluster for their WEB service, so each time you log in there, you end up accessing a server with a different DNS name (something like https://websmp104.sap-ag.de/support). That means that your browser will not be able to store your credentials because every time you attempt to connect to https://service.sap.com/support, the DNS name changes so it pops up a dialog asking for login data again and again... Perhaps this is SAP's way of implementing the ultimate security system but, as far as I can say it is very annoying.

Anyway, once you are there select "Download" from the top menu. Next click "Search on all categories" from the left menu and enter RFC SDK on the search box that appears. You will be given the chance to select SAP RFC SDK 6.40 from the results page. Be careful not to choose the UNICODE version. Select Linux on x86_64 64bit from the list of architectures and you will end up with an SAR file in your download basket. Now you can download it

There is one more problem though. The file you download is of type SAR. meaning SAP Archive. In order to unpack it you will need SAPCAR, SAP's unpacking program. You download this the same way you downloaded RFCSDK -- just type SAPCAR on the search box. Only thing is that the Linux x86_64 version does not run on CentOS. You will need to download a Windows version, unpack the archive on a Windows machine and then upload it again on you Linux system. At least that is what I had to do. (From what I was able to understand SAP's SAPCAR for Linux is compiled to run under SuSE, so if you have satch a machine, you can try unpacking the archive over there...)

Installation

So now let's assume that you have placed SAP's RFC SDK under /opt/SAP/rfcsdk and SAPRFC extention module for PHP under /usr/src/saprfc-1.4.1/. Type the following commands on your shell prompt or add them at the end of your startup file. (I put them in /etc/profile.)

# SAP RFC SDK
export SAPRFC_DIR=/opt/SAP/rfcsdk/
export PATH=$SAPRFC_DIR/bin:$PATH

If necessary, log out and back in again. Now move to the SAPRFC extension for PHP directory and issue the phpize command. This will create the configure script that needs to be run next. After configure completes, run make and make install (as root) to finish installation. When everything finishes the file saprfc.so will be placed in your /usr/lib64/php/modules folder. Open you php.ini file located in /etc and add a line line

extension=saprfc.so

in the Dynamic Extensions section, save it, restart http server and you are ready to go.

[root@barbara ~]# service httpd restart
Stopping httpd:                                            [  OK  ]
Starting httpd:                                            [  OK  ]

Verification and testing

The very first check will be as simple as looking at the saprfc section of your phpinfo() page. You should be seeing something like :

The next thing will be to write an actual program that does something more practical like connecting to an SAP R/3 system and fetching back some useful data. Since this already a rather lengthy post, I will prepare and provide a test program some time later.

One last comment: It took me a while to figure that out. All examples that come along with the SAPRFC module for PHP, as well as the examples on the "SAP Developer's Guide to PHP" book, use <? instead of <?php to introduce php code. This is going to give you a lot of trouble when attempting to use these files with php 5.3.1 so before trying anything else, go to the files in the installation directory -- especially saprfc.php that you will always include -- and perform the necessary changes.

Friday, 27 November 2009

Enterprise class hardware for Enterprise class software

We wanted to set up a portable LAMP server for a home project we are playing with and there it is :

Since the little monster uses two SD cards as hard drives, it takes around four minutes to boot, but once it does boot, then its 900MHz Celeron CPU with its 1GB of RAM can stand up against a multi-threaded Java program gathering information from the local Ethernet and a complete Apache, PHP and MySQL server accessed for reporting.

Only thing is that the book is almost three times thicker than the machine.