A model layer for the i5 Toolkit on PHP

PHP has kind of taken the i5 world by storm.  Many RPG developers want to move their applications to the web, but while there are options for doing so, RPG is not all that well suited for writing web applications unless you invest heavily in a CGI framework like DEV2.  The other option has been Java, which represents a completely different way of coding that many RPG developers are not overly comfortable with.  Alongside that, there is the standard issue of Java taking some time to learn properly.  So along comes PHP and it gives the RPG developers a chance to ease into programming for the web.  They can still use procedural programming when they start, but they can also move to more structured applications as they become more familiar with the language.

However, there is often a problem in that there is a significant amount of functionality that still exists in RPG that you don't necessarily want to duplicate in PHP.  For that reason the PHP Toolkit was added as part of Zend Core, and now Zend Server for the i.  You can see an example of that on George Papayiannis' blog.  What it basically amounts to is calling several PHP tookit functions whenever you want to access that functionality.  The example from George's blog is like this:

$conn = i5_connect("localhost", "gpapayia", "secret");

if (!$conn) {
    throw_error("i5_connect");
    exit();
}

$description = array(
    array(
        "Name"=>"PROD_ID",
        "IO"=>I5_IN,
        "Type"=>I5_TYPE_CHAR,
        "Length"=>"7"
    ),
    array(
        "Name"=>"STORE_LOC",
        "IO"=>I5_IN,
        "Type"=>I5_TYPE_CHAR,
        "Length"=>"10"
    ),
    array(
        "Name"=>"PRICE",
        "IO"=>I5_INOUT,
        "Type"=>I5_TYPE_PACKED,
        "Length"=>"5.2"
    ),
);

$pgm = i5_program_prepare("QGPL/GEOPGRM", $description);

if (!$pgm) {
    throw_error("i5_program_prepare");
    exit();
}

$parameter = array(
    "PROD_ID"=>"xyz101",
    "STORE_LOC"=>"a1001",
    "PRICE"=>0.00
);

$parmOut = array(
    "PROD_ID"=>"PROD_ID",
    "STORE_LOC"=>"STORE_LOC",
    "PRICE"=>"AMOUNT",
);

$ret = i5_program_call($pgm, $parameter, $parmOut);

if (!$ret) {
    throw_error("i5_program_call");
    exit();
}

echo "Product Id: ".$PROD_ID."l
echo "Store Location: ".$STORE_LOC.";
echo "Price: ".$AMOUNT.";

Before I get into some of the problems with this I would like to state that this is not a critique of George's code.  Given the toolkit as it stands, this is, in actuality, a good example.

However, there are a couple of problems with this.

  1. This can lead to a lot of duplicated code
  2. This injects individual variables into the global scope. (due to the toolkit)
  3. It is difficult to structure a large application when you have to repeat the same connectivity code over and over again.

So what I've done is written a very simple library that can be used to abstract out that functionality.  I have placed it on Github so you can install it, examine it, and modify it to add functionality as needed.  The library I wrote only supports program calls at this time and there is a lot of additional functionality in the i5 Toolkit that could be abstracted.

The way it works is that you set up a connection manager in one part of your application, either an include file or preferably some kind of bootstrap.  Then you define your individual model classes (classes that represent programs in RPG) to match that functionality.  Then you call it.  I won't spend any time at this point talking about the underlying architecture.  If you are interested you can read the source code.  Or I may do another blog post on it.

The first thing you need to do is define your connection manager.  This allows you to set the adapter type and any connection information in one place.  This would be done in a bootstrap or include file.

$mgr = new Itk_Connection_Manager(
    array(
        'adapter'        => 'Live',
        'username'        => '',
        'password'        => '',
        'host'            => '127.0.0.1'            
    )
);
Itk_PgmAbstract::setDefaultAdapter($mgr->getAdapter());

What this does is set the default adapter for any model classes that are called.  You can actually create a model object off of any arbitrary adapter (if you have multiple adapters running at the same time), but this generally will not be necessary.  In this library I have created two different adapters.  One is Live, which will call the i5 Toolkit.  The other is Mock, which allows you to define the return values for individual program calls to help facilitate unit testing.  Look in the /tests directory for some examples of this.

The next thing to do is to define your model class.

class Model_CommonPgm extends Itk_PgmAbstract
{
    protected $_programName = 'ZENDSVR/COMMONPGM';
    protected $_description = array(
        'CODE'    => array(
            self::DESC_IO        => I5_INOUT,
            self::DESC_TYPE      => I5_TYPE_CHAR,
            self::DESC_LENGTH    => "10"
        ),
        'DESC'    => array(
            self::DESC_IO        => I5_INOUT,
            self::DESC_TYPE      => I5_TYPE_CHAR,
            self::DESC_LENGTH    => "10"
        )
    );
    public function setCode($code)
    {
        $this->CODE = $code;
    }
}

What you are doing here is defining what the input and output parameters are, just like you would in a regular i5 toolkit call.  However, there is a big difference in the actual call.  The class definitions are usually done in separate files and included either manually or (preferably) using an autoloader, such as Zend_Loader_Autoloader.  So actually calling the RPG function makes your inline code extremely simple.  Additionally you can optionally define setters like I did here to document and structure the available functionality even more.

One of the reasons that I like this is that it is both very clean and very repeatable.  When you want to actually execute the RPG logic from within your application you run code like this.

$model = new Model_CommonPgm();
$model->setCode($_POST['code']);
$result = $model->execute();
echo "The return values are: ", "Code: ", $result->CODE, " Description: ", $result->DESC," ";

The code example I took this from was at least 2 dozen lines long and I was able to bring it down to about 3 lines of code.  What this is doing is creating a new class and attaching outside data to the IN or INOUT properties by referencing the POST variable.  Then I execute it, taking the result and putting it into a result object where I can reference the out-bound results of the call.

Please note that I am not an RPG programmer and as such it would probably be a good idea for people more familiar with RPG than I to use this and find problems but I think that this can go a long way to making PHP applications that rely on RPG and the i5 Toolkit to be much more structured and much easier to maintain.

Again, here's the link to Github.

Related posts

10 thoughts on “A model layer for the i5 Toolkit on PHP

  1. KP

    Hi,

    Thank you for this description. I would really want to call iSeries programs using Itk. Unfortunately I receive the following error :

    Fatal error: Class ‘Itk_Connection_Manager’ not found in /www/zendcore/htdocs/Expeditie/application/Bootstrap.php on line 12

    I have added the ITK classes to the PHP.INI file :

    include_path=”.:/usr/local/Zend/Core/share/pear:/usr/local/Zend/ZendFramework/library:/www/zendcore/htdocs/Itk/library”

    but still it does not work… Any idea?

    Thanks you,
    KP

  2. Kevin

    It sounds like you may not have set up an autoloader yet. http://framework.zend.com/manual/en/zend.loader.autoloader.html

  3. Hi Kevin,

    Thank you for the quick response!

    I seem to get a little bit further when adding include_once for all the classes in the bootstrap :

    class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
    {
    protected function _initAutoload()
    {
    $autoloader = new Zend_Application_Module_Autoloader(array(
    ‘namespace’ = ‘Application’,
    ‘basePath’ = dirname(__FILE__),
    ));

    include_once(‘Itk/Exception.php’);
    include_once(‘Itk/PgmAbstract.php’);
    include_once’Itk/Connection/AdapterAbstract.php’); include_once(‘Itk/Connection/Exception.php’);
    include_once(‘Itk/Connection/Manager.php’);
    include_once(‘Itk/Connection/Adapter/Exception.php’);
    include_once(‘Itk/Connection/Adapter/Live.php’);
    include_once(‘Itk/Connection/Adapter/Mock.php’);
    include_once(‘Itk/Pgm/Exception.php’);
    include_once(‘Itk/Pgm/Result.php’);

    $mgr = new Itk_Connection_Manager(
    array(
    ‘adapter’ = ‘Live’,
    ‘username’ = ”,
    ‘password’ = ”,
    ‘host’

  4. Kevin

    Try doing it without the include_once (and technically you should be using require_once since it will cause a fatal error if the file is not found). The thing you don’t want to do is become dependent on those. Instead, add this line to your configuration file (seeing that you are using Zend_Application).

    autoloaderNamespaces[] = “Itk”

  5. KP

    Great! The first problem is solved…

    Now I only get the following message when calling the execute function of my class:

    Message: Unable to execute program: Array

    This is my class :

    class Model_TestCall extends Itk_PgmAbstract
    {
    protected $_programName = ‘KPLK/TESTCALL’;
    protected $_description = array(
    ‘CODE’ = array(
    self::DESC_IO = I5_INOUT,
    self::DESC_TYPE = I5_TYPE_CHAR,
    self::DESC_LENGTH = “10”
    ),
    ‘DESC’ = array(
    self::DESC_IO = I5_INOUT,
    self::DESC_TYPE = I5_TYPE_CHAR,
    self::DESC_LENGTH = “10”
    )
    );

    public function setCode($code)
    {
    $this-CODE = $code;
    }
    }

    This is the code where I call the iSeries program :

    include_once ‘TestCall.php’;
    $testcall=new Model_TestCall;
    $testcall-setCode(‘1’);
    $result=$testcall-execute();

    And this is my iSeries program :

    d TestCall pr extpgm(‘TESTCALL’)d P_CODE 10
    d P_DESC 10

    d TestCall pi
    d P_CODE 10
    d P_DESC 10

    /free
    Select;
    When P_CODE=’1′;
    P_DESC=’Kode 1′;
    When P_CODE=’2′;
    P_DESC=’Kode 2′;
    Other;
    P_DESC=’Unknown’;
    Endsl;
    *INLR=’1′;
    /End-free

    Thanks again…

  6. Kevin

    Try downloading a new copy of the library. I think that i5_error(), which is what is called when an error occurs, returns an array. The updated Live adapter uses i5_errormsg() instead. That should give you an actually useful error message.

  7. KP

    Thanks again!

    I have downloaded the new version and now I get a better error message saying that the program was not found, and indeed, it was placed in the wrong lib.

    Now it seems to be working!

    Greetings,
    KP

  8. kp

    Hello,

    Me again… Now I want to call a program with a numeric parameter, but the parameter is bad received by the program.
    In the dump, I can see the following :

    P_CODENUM PACKED(4,0) 0002. ‘000020’X

    I believe it must be somthing like :

    P_CODENUM PACKED(4,0) 0002. ‘00002F’X

    I already tried I5_TYPE_LONG/ I5_TYPE_SHORT/ I5_TYPE_DOUBLE/ I5_TYPE_LONG8/ I5_TYPE_NUMERICCHAR all without success…

    Another question is : how can you define the number of decimal places?

    Any suggestions?

  9. Kevin

    I notice that you didn’t list a floating point type. Check out the docs at http://files.zend.com/help/Zend-Core-i5/easycom_php_data_description.htm and see if some of those work.

  10. KP

    Indeed! I found i have to use I5_TYPE_PACKED…

    Thx…

Leave a Comment