Wednesday, June 1, 2011

Wish List Sample Application:: PDO Flavor

After Jeffrey Rubinoff asked community for help on rewriting sample wish list application accompanying his tutorial Creating a Database Driven Application With NetBeans IDE PHP Editor number of users raised question why PDO was not used instead of mysqli and OCI8.

During the NetCat 7.0 I had a chance to review Jeff's tutorial. I could not resist to try installing PDO_OCI. Having everything needed running on my Ubuntu box, I was finally able to do what I promised: Create PDO database class. The key objective was to make it easy to switch between MySQL and Oracle XE, preferably just by changing connection string.

Problems

Before porting WishDB class to PDO due to differences between Oracle XE and MySQL queries needed to be normalized. There are two differences affecting Wish List Sample Application:

  • Oracle is case insensitive to object names, and Oracle schema object names are stored as uppercase
  • There is a difference in handling dates between MySQL and Oracle XE
Further on, in few cases database specific PHP functions where used outside WishDB class. Such usage is hardening code maintenance and porting code to PDO. Therefore those cases had to be resolved as well before porting WishDB class.
Finally, if data could be retrieved in a single query don't query database multiple times. Establishing connection with database is bottleneck in every PHP database driven application.

Restructuring the Code

Here is an example:
// MySQL version
        $wisher = $this->query("SELECT id FROM wishers WHERE name = '"
                        . $name . "'");

        // OCI8 version
        $query = "SELECT ID FROM wishers WHERE name = :user_bv";

        // PDO version
        $query = "
            SELECT id ID
            FROM wishers
            WHERE name = :user_bv
            ";
Schema object names in MySQL version of the application are written lowercase. Oracle XE will not have a problem with lowercase names but returned array keys will be uppercase. I decided to add uppercase column names alias and use OCI8 version of the application as base for refactoring. In this way there is no need to change MySQL schema nor PHP code other then WishDB class.

Handling dates is a bit trickier. Both MySQL and Oracle XE store dates internally as timestamp. However, input and output need to be formatted differently. Logical step was to create functions having same names, input parameters and outputting identically formatted date string.

Before creating SQL functions I had to normalize function format_date_for_sql first. I prefer using DateTime class over native PHP functions.
// MySQL version
    function format_date_for_sql($date) {
        if ($date == "")
            return null;
        else {
            $dateParts = date_parse($date);
            return $dateParts['year'] * 10000 + $dateParts['month'] * 100 + $dateParts['day'];
        }
    }

    // OCI8 version
    function format_date_for_sql($date) {
        if ($date == "")
            return null;
        else {
            $dateParts = date_parse($date);
            return $dateParts['year'] * 10000 + '-' + $dateParts['month'] * 100 + '-' + $dateParts['day'];
        }
    }

    // PDO version
    function format_date_for_sql($date)
    {
        if ($date == "") {
            return null;
        } else {
            $dateTime = new DateTime($date, new DateTimeZone("UTC"));
            return $dateTime->format("Y-n-j H:i:s e");
        }
    }


Here are those two SQL functions:

// MySQL version
CREATE DEFINER=`root`@`localhost` FUNCTION  `wishlist`.`format_due_date`(
`in_date` VARCHAR(255) CHARSET latin1
) RETURNS varchar(255) CHARSET latin1
    SQL SECURITY INVOKER
BEGIN
return CONCAT(in_date, SPACE(1), 'UTC');
END $$

CREATE DEFINER=`root`@`localhost` FUNCTION  `wishlist`.`set_due_date`(
`in_date` VARCHAR(255) CHARSET latin1
) RETURNS varchar(255) CHARSET latin1
    SQL SECURITY INVOKER
BEGIN
return SUBSTR(in_date, 1, length(in_date) - 4);
END $$

// Oracle XE version
CREATE OR REPLACE
FUNCTION "FORMAT_DUE_DATE" (in_date in TIMESTAMP) return VARCHAR2 is
begin
return TO_CHAR(IN_DATE, 'YYYY-MM-DD HH24:MI:SS TZR');
end; /

CREATE OR REPLACE
FUNCTION "SET_DUE_DATE" (in_date in VARCHAR2) return VARCHAR2 is
begin return TO_TIMESTAMP_TZ(in_date, 'YYYY-MM-DD HH24:MI:SS TZR');
end; /
Finally queries could be normalized.
// MySQL version
        $this->query("INSERT INTO wishes (wisher_id, description, due_date)" .
                " VALUES (" . $wisherID . ", '" . $description . "', "
                . $this->format_date_for_sql($duedate) . ")");

        // OCI8 version
        $query = "INSERT INTO wishes (wisher_id, description, due_date) VALUES (:wisher_id_bv, :desc_bv, to_date(:due_date_bv, 'YYYY-MM-DD'))";

        // PDO version
        $query = "
            INSERT INTO wishes (wisher_id, description, due_date)
            VALUES (
                :wisher_id_bv,
                :desc_bv,
                set_due_date(:due_date_bv)
                )
            ";
User's wish list is retrieved in two steps: First, a user #id is found by user name and later on list of wishes is found by a user #id.
$wisherID = WishDB::getInstance()->get_wisher_id_by_name($_SESSION['user']);
                $stid = WishDB::getInstance()->get_wishes_by_wisher_id($wisherID);
User's wish list could be retrieved in a single query. Oracle XE supports full outer join but, unfortunately, MySQL does not. Therefore, a query has to be a bit more complex.
SELECT w.id ID, w.description DESCRIPTION,
            format_due_date(w.due_date) DUE_DATE, wr.id WRID
            FROM wishes w RIGHT OUTER JOIN wishers wr
            ON wr.id = w.wisher_id
            WHERE wr.name = :user_bv
Finally, I replaced function get_wishes_by_wisher_id with function get_wishes_by_wisher_name
/**
     * Gets user's wishes for the user having given name
     *
     * @param string $name
     * @return ArrayIterator
     */
    public function get_wishes_by_wisher_name($name)
    {
        $query = "";
        $stid = null;
        $row = array();
        $result = null;

        $query = "
            SELECT w.id ID, w.description DESCRIPTION,
            format_due_date(w.due_date) DUE_DATE, wr.id WRID
            FROM wishes w RIGHT OUTER JOIN wishers wr
            ON wr.id = w.wisher_id
            WHERE wr.name = :user_bv
            ";

        $stid = $this->con->prepare($query);
        $stid->bindParam(":user_bv", $name, PDO::PARAM_STR);
        $stid->execute();

        $result = new ArrayIterator();
        while ($row = $stid->fetch(PDO::FETCH_ASSOC)) {
            $result->append($row);
        }
        $result->rewind();

        return $result;
    }
Function get_wishes_by_wisher_name is returning ArrayIterator instead of resultset, letting me to use the same code regardless of database sitting in backend and having more clear code when displaying results.
<table class="std">
            <tr>
                <th>Item</th>
                <th>Due Date</th>
                <th colspan="2">&nbsp;</th>
            </tr>
            <?php
            $wishes = WishDB::getInstance()->
                    get_wishes_by_wisher_name($_SESSION["user"]);
            while ($wishes->valid()):
                $row = $wishes->current();
                $date = new DateTime($row['DUE_DATE'], new DateTimeZone("UTC"));
                if (true === is_null($row["ID"])) {
                    $wishes->next();
                    continue;
                }
                ?>
                <tr>
                    <td>&nbsp;
                        <?php
                        echo htmlentities($row['DESCRIPTION']);
                        ?>
                    </td>
                    <td>&nbsp;
                        <?php
                        echo (is_null($row['DUE_DATE']) ?
                                "" : $date->format("Y, M jS"));
                        $wishID = $row['ID'];
                        ?>
                    </td>
                    <td>
                        <form name="editWish"
                              action="editWish.php" method="GET">
                            <input type="hidden"
                                   name="wishID"
                                   value="<?php echo $wishID; ?>" />
                            <input type="submit" name="editWish" value="Edit" />
                        </form>
                    </td>
                    <td>
                        <form name="deleteWish"
                              action="deleteWish.php" method="POST">
                            <input type="hidden"
                                   name="wishID"
                                   value="<?php echo $wishID; ?>" />
                            <input type="submit"
                                   name="deleteWish"
                                   value="Delete" />
                        </form>
                    </td>
                </tr>
                <?php
                $wishes->next();
            endwhile;
            ?>
        </table>
You can grab the code from Kenai repository.

There is more to be done, for example all input variables must be filtered. You are warmly welcomed to join Wish List Sample Application project.

Saturday, April 9, 2011

NetBeans IDE 7.0 Release Candidate 2 Available for Download

The NetBeans Team announced that the second release candidate build of NetBeans IDE 7.0 is available for download.

Join on Tuesday, April 12th at 6PM GMT, NetBeans Engineering Manager John Jullion-Ceccarelli for a LIVE Oracle TechCast about NetBeans IDE 7.0, which introduces language support for Java SE 7 and provides other feature enhancements. 

There'll be a Q&A segment where viewers can pose questions. Questions can also be posted to Twitter with the hashtag #techcastlive.

The final release of NetBeans IDE 7.0 is planned for April.

Tuesday, April 5, 2011

Installing PDO_OCI on Ubuntu 10.10 x64

The PHP Data Objects (PDO) extension defines a lightweight, consistent interface for accessing databases in PHP.
The PDO provides a data-access abstraction layer that can be used with a variety of databases, giving the flexibility of changing the database backend, without having to alter access methods. Even when using a single RDBMS, the PDO can provide advantages, for example, if using the MySQL, the same data-access methods can be used regardless of the MySQL version.
I reviewed recently the chapter Lesson 1b: Creating Oracle Database Tables from Jeffrey Rubinoff's tutorial Creating a Database Driven Application With PHP, as part of the NetBeans NetCAT 7.0 program. To meat requirements I had to install Oracle XE, Instant Client and the PHP OCI8 extension.
I fancy PDO over all other options. Having Oracle XE running on my HP was a chance to try and test PDO_OCI driver.

If running 32bit Ubuntu read no more: All you need is The Underground PHP and Oracle Manual.

Getting PDO_OCI driver source code and patching conifg.m4

Grab the current PHP snapshot. A patch to PDO_OCI config.m4 for 64bit RPM support was merged on March 30th. See the Bug #44989. Over the time this change will be integrated and available in the downloads section
Extract from the downloaded archive the pdo_oci folder residing in the ext folder. 
Open config.m4 located in the extracted pdo_oci folder. 
If using the NetBeans IDE, ctrl+3 to open Favorites, browse to the extracted pdo_oci folder, right click on config.m4 and in the context menu click Open.


Verify that the date in the SVN $Id on the line one is equal or greater then 2011-03-29.
dnl $Id: config.m4 309818 2011-03-29 18:40:20Z felipe $
Download, save and apply patch config.m4.deb.patch attached to the Bug #54451
If using the NetBeans IDE, after downloading the patch, make sure that focus is on previously opened config.m4, click Apply Diff Patch in Tools menu, browse to the downloaded patch, select it and click Patch.
Say yes when asked to view applied changes and verify that config.m4 is patched correctly.
Move the folder pdo_oci containing patched config.m4 to /usr/local/src
If using the NetBeans IDE open Terminal from Window > Output > Terminal and type:
$ sudo mv ./wherever/is/pdo_oci /usr/local/src

Getting Oracle Instant Client and additional header files

Download from the Instant Client Downloads for Linux x86-64 page instantclient-basiclite-linux-x86-64-11.2.0.2.0.zip and instantclient-sdk-linux-x86-64-11.2.0.2.0.zip.
Extract content of those two archives to /opt/oracle.
If using the NetBeans IDE open Terminal from Window > Output > Terminal and type:
$ sudo unzip ./wherever/is/instantclient-basic-linux-x86-64-11.2.0.2.0.zip -d /opt/oracle
$ sudo unzip ./wherever/is/instantclient-sdk-linux-x86-64-11.2.0.2.0 -d /opt/oracle
# make symbolic links
$ sudo ln -s /opt/oracle/instantclient_11_2/libclntsh.so.11.1 /opt/oracle/instantclient_11_2/libclntsh.so
$ sudo ln -s /opt/oracle/instantclient_11_2/libocci.so.11.1 /opt/oracle/instantclient_11_2/libocci.so
Create new file oracleinstantclient.conf in /etc/ld.so.conf.d, add line /opt/oracle/instantclient_11_2, save, close and run ldconfig.
sudo nano /etc/ld.so.conf.d/oci8.conf
sudo ldconfig

make and make install

If using the NetBeans IDE open Terminal from Window > Output > Terminal and type:
$ cd /usr/local/src/pdo_oci/
$ phpize
$ ./configure
$ make
$ make install
Don't run make test. All test will fail, since PDO extension is not loaded in generated php.ini. 
Verify that pdo_oci.so is in your extension directory. Default path is /usr/lib/php5/20090626/pdo_oci.so.

Load PDO_OCI extension and restart Apache

Create new file pdo_oci.ini in /etc/php/apache2/conf.d, add line extension=pdo_oci.so, save close and restart Apache.
$ sudo nano /etc/php5/apache2/conf.d/pdo_oci8.ini
$ sudo /etc/init.d/apache2 restart

If the NetBeans IDE user grab database structure and insert records as described in the chapter Lesson 1b: Creating Oracle Database Tables from Jeffrey Rubinoff's tutorial Creating a Database Driven Application With PHP. Create new PdoOciTest.php file in the web root of the text application.

<html>
    <head>
        <title>PDO OCI Test Page</title>
    </head>

    <body>
        <?php
        $pdo = new PDO(
                        'oci:dbname=localhost/XE',
                        'jim',
                        'mypassword'
        );
        $sql = "
            SELECT DEPARTMENT_NAME, MANAGER_ID, LOCATION_ID, STREET_ADDRESS,
            POSTAL_CODE, CITY, STATE_PROVINCE
            FROM departments NATURAL JOIN locations
            ORDER by DEPARTMENT_NAME
            ";
        $stmt = $pdo->query($sql);
        $result = $stmt->fetchAll();
        ?>
        <table>
            <?php
            foreach ($result as $row) {
                ?><tr><?php
                foreach ($row as $item) {
                    ?><td><?php
                    echo ($item !== null ? htmlentities($item, ENT_QUOTES) : "&nbsp;");
                    ?></td><?php
                }
                ?></tr><?php
            }
            ?>
        </table>
    </body>
</html>
Having focus on the created file go to Run > Run File or press space+F6. The file will run on the defined local web server and open in the default browser,