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.