Programming in PHP: Axis Entity Object Helper

Our Axis Entity Object class is quite powerful and useful for lightweight ORM. But lets go ahead and create a help class that will alleviate a lot of business and view coding by automating the create and update forms and processing for Axis Entity Objects.

First lets look at how the code will operate in the business side of things. Here is how we generate the creation form for our example Axis Entity Object class “Fruit”:


<form action="index.php" method="post">
    <?php AxisObjectHelper::DrawFormCreate('Fruit'); ?>
    <input type="submit" />
</form>

What you can see here is that our helper class will contain a static method for creating an update form for our class “Fruit”. Lets take a look at that code.


    public static function DrawFormCreate($class)
    {
        // get our fields from our mapped database table
        $fields = AxisObjectHelper::GetFields($class);

        // loop through the returned fields and generate input fields matching the name of the columns.
        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                echo '<div class="object_field" id="create_'.$class.'_field_'.$field_object->Field.'">'.self::PrettifyName($field_object->Field).'</div>';

                echo '<div class="object_input" id="create_'.$class.'_input_'.$field_object->Field.'">'.self::DrawInput($field_object).'</div>';
            }
        }
    }

Generating the create form is simple as pie once we have the corresponding columns of the class.

Here is how we process the create form:


AxisObjectHelper::ProcessFormCreate('Fruit');

So simple! Again we have a single line of PHP for processing the user’s input and creating the object. Lets take a closer look at that static method.


    public static function ProcessFormCreate($class)
    {
        // get our table columns
        $fields = AxisObjectHelper::GetFields($class);

        // create a new instance of our Axis Entity Object
        $create = new $class();

        // loop through the fields and create them
        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                $field_name = $field_object->Field;
                $create->$field_name = $_POST[$field_name];
            }
        }

        return $create->Create();
    }

Using the Axis Entity Object class, we leverage it’s ORM to create the object very simply.

Updating objects is just as simple requiring only two lines of PHP to generate the form and to process it. Here is the example helper class in its entirety.


class AxisObjectHelper
{
    public static function OpenMySQL($host, $db, $user, $pass)
    {
        mysql_connect($host, $user, $pass);
        mysql_select_db($db);
    }

    public static function DrawFormCreate($class)
    {
        // get our fields from our mapped database table
        $fields = AxisObjectHelper::GetFields($class);

        // loop through the returned fields and generate input fields matching the name of the columns.
        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                echo '<div class="object_field" id="create_'.$class.'_field_'.$field_object->Field.'">'.self::PrettifyName($field_object->Field).'</div>';

                echo '<div class="object_input" id="create_'.$class.'_input_'.$field_object->Field.'">'.self::DrawInput($field_object).'</div>';
            }
        }
    }

    public static function ProcessFormCreate($class)
    {
        // get our table columns
        $fields = AxisObjectHelper::GetFields($class);

        // create a new instance of our Axis Entity Object
        $create = new $class();

        // loop through the fields and create them
        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                $field_name = $field_object->Field;
                $create->$field_name = $_POST[$field_name];
            }
        }

        return $create->Create();
    }

    public static function DrawFormUpdate($object)
    {
        $class = get_class($object);
        $primary_key_column = strtolower($class).'_id';

        $fields = AxisObjectHelper::GetFields($class);

        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                $field_name = $field_object->Field;
                $object_field_value = $object->$field_name;

                echo '<div class="object_field" id="update_'.$class.'_field_'.$field_object->Field.'_'.$object->$primary_key_column.'">'.self::PrettifyName($field_object->Field).'</div>';

                echo '<div class="object_input" id="update_'.$class.'_input_'.$field_object->Field.'_'.$object->$primary_key_column.'">'.self::DrawInput($field_object, $object_field_value).'</div>';
            }
        }
    }

    public static function ProcessFormUpdate($class, $object_id)
    {
        $fields = AxisObjectHelper::GetFields($class);
        $update_object = call_user_func(array($class, 'Factory'), $object_id, $class);

        foreach($fields as $field_object)
        {
            if($field_object->Key != 'PRI')
            {
                $field_name = $field_object->Field;
                $update_object->$field_name = $_POST[$field_name];
            }
        }

        return $update_object->Update();
    }

    public static function DrawInput($field_object, $value = '')
    {
        // cases for input
        if(strpos($field_object->Type, 'tinyint') !== false && strpos($field_object->Type, '(1)') !== false)
        {
            if($value != '')
            {
                if((bool)$value)
                {
                    return '<input type="checkbox" class="input_boolean" name="'.$field_object->Field.'" value="1" checked="checked" />';
                }
            }

            return '<input type="checkbox" class="input_boolean" name="'.$field_object->Field.'" value="1" />';
        }

        if  (
            strpos($field_object->Type, 'int') !== false ||
            strpos($field_object->Type, 'varchar') !== false ||
            strpos($field_object->Type, 'float') !== false ||
            strpos($field_object->Type, 'decimal') !== false ||
            strpos($field_object->Type, 'double') !== false ||
            strpos($field_object->Type, 'real') !== false ||
            strpos($field_object->Type, 'date') !== false ||
            strpos($field_object->Type, 'time') !== false
            )
        {
            return '<input type="text" class="input_varchar" name="'.$field_object->Field.'" value="'.htmlentities($value).'" />';
        }

        if(strpos($field_object->Type, 'text') !== false)
        {
            return '<textarea class="input_text" name="'.$field_object->Field.'">'.$value.'</textarea>';
        }

    }

    public static function PrettifyName($name)
    {
        return ucwords(str_replace('_' , ' ', $name));
    }

    // MySQL Specific
    public static function GetFields($class)
    {
        $primary_key_column = strtolower($class).'_id';
        $table = DB_T_PREPEND.strtolower($class);

        $cmd = "SHOW COLUMNS FROM ".$table."";

        $result = mysql_query($cmd);
        while($row = mysql_fetch_object($result))
        {
            $cols[] = $row;
        }

        return $cols;;
    }

}

This helper class is simply used as an example for automating CRUD using Axis Entity Objects. We can easily modify the methods apply business rules for validating the object by providing that method in our “Fruit” class and calling its Validate() method. We can also adjust the methods for other databases.

That is our limitation with this example helper. Each database has different ways of parsing the columns of a table. This example was specific to MySQL.

Programming in PHP: Axis Entity Objects

During some spare time I developed an abstract ORM class that can be extended by entity classes that automatically implement the create, update and delete methods for the object’s table entry in a relational database. This is a work in progress, but I will go through the existing version to explain.

The create, update, and delete methods can be called on any object that extends the AxisEntityObject class to perform the related operation on that instance. It will immediately remove, create or update the object related entry in the database using created SQL and PHP PDO. But also, each class inherits a static method that is the factory for the object type which requires only the primary key associated with an object to generate it.

Lets take a look at the interface.

<?php
interface IAxisEntityObject{   
    public function Create();
    public static function Factory($primary_key, $custom_class_name = '');
    public static function FactoryList($primary_keys, $custom_class_name = '');
    public static function FactoryAll();       
    public function Update();
    public function Delete();
}

As you can see we require the defined methods and also require three types of factories that are common in ORM object generation. We can product all objects of a class, a single object by primary key, or a list based on an array of primary keys.

Here is the implementation of the abstract class.


abstract class AxisEntityObject implements IAxisEntityObject
{

    //////////////////////////////////
    // CUD METHODS
    ////////////////////////////////

    public function Create()
    {
        // We need to grab the current objects class before we call its functions
        // so that we can link to the related table and not to a table named after our abstract class

        $class = get_class($this);
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        $entry_fields = array();

        // Loop through each of the fields of the object instance
        // and look for any other properties that need to cascade the creation process

        foreach($this as $field => $value)
        {
            if($field != $primary_key_column)
            {
                if($value instanceof AxisEntityObject)
                {
                    $value->Create();
                }
                elseif(is_array($value))
                {
                    foreach($value as $array_value)
                    {
                        if($array_value instanceof AxisEntityObject)
                        {
                            // Cascade call all properties Create method if they are of the same class type
                            // recursive in nature

                            $array_value->Create();
                        }
                    }
                }
                elseif($this->$field != null)
                {
                    // Build our SQL

                    $k_sql .= $field.', ';

                    $v_sql .= ':'.$field.', ';

                    $entry_fields[$field] = $value;
                }
            }
        }

        // Use PHP PDO to persist the objects fields and complete the create method
        $cmd = "INSERT INTO ".$table." (".substr($k_sql, 0, -2).") VALUES (".substr($v_sql, 0, -2).")";

        $STH = $GLOBALS['DBH']->prepare($cmd);
        $STH->execute((array)$entry_fields);

        return $GLOBALS['DBH']->lastInsertId();
    }

    public function Update()
    {
        // Again for updates we need the class name that extends AxisEntityObjects
        $class = get_class($this);
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        // Cascade the update call to all properties that are also extend the abstract class

        if($this->$primary_key_column != '')
        {

            foreach($this as $field => $value)
            {
                if($field != $primary_key_column)
                {
                    if($value instanceof AxisEntityObject)
                    {
                        $value->Update();
                    }
                    elseif(is_array($value))
                    {
                        foreach($value as $array_value)
                        {
                            if($array_value instanceof AxisEntityObject)
                            {
                                $array_value->Update();
                            }
                        }
                    }
                    elseif($value != null)
                    {
                        $v_sql .= $field . ' = :'.$field.', ';

                        $entry_fields[$field] = $value;
                    }
                }
            }

            // Persist the object changes
            $cmd = "UPDATE ".$table."  SET ".substr($v_sql, 0, -2)." WHERE ".$primary_key_column." = ".$this->$primary_key_column;

            $STH = $GLOBALS['DBH']->prepare($cmd);
            $STH->execute((array)$entry_fields);
        }
    }

    public function Delete()
    {
        $class = get_class($this);
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        if($this->$primary_key_column != '')
        {
            $cmd = "DELETE FROM ".$table." WHERE ".$primary_key_column." = ".$this->$primary_key_column;

            $STH = $GLOBALS['DBH']->prepare($cmd);
            $STH->execute((array)$this);
        }
    }

    //////////////////////////////////
    // RETRIEVE METHODS
    ////////////////////////////////

    public static function Factory($primary_key, $custom_class_name = '')
    {
        // Get our proper class name and table names
        if($custom_class_name != '')
            $class = $custom_class_name;
        else
            $class = get_called_class();
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        // get table entry for object
        $cmd = "SELECT
                    *
                FROM
                    `".$table."`
                WHERE
                    `".$primary_key_column."` = '".$primary_key."'";

        // Execute call
        $STH = $GLOBALS['DBH']->prepare($cmd);
        $STH->setFetchMode(PDO::FETCH_CLASS, $class);
        $STH->execute();
        $result = $STH->fetch(PDO::FETCH_CLASS);

        if($result == false)
            return null;

        return $result;
    }

    public static function FactoryAll()
    {
        // get the proper class
        $class = get_called_class();
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        $cmd = "SELECT ".$primary_key_column." FROM ".$table;

        $STH = $GLOBALS['DBH']->prepare($cmd);
        $STH->execute();

        // Call our Factory method on all returned objects mapped
        while($row = $STH->fetch(PDO::FETCH_OBJ))
        {
            $list[] = call_user_func(array($class, 'Factory'), $row->$primary_key_column, $class);
        }

        return $list;
    }

    public static function FactoryList($primary_keys, $custom_class_name = '')
    {
        // get our proper class
        if($custom_class_name != '')
            $class = $custom_class_name;
        else
            $class = get_called_class();
        $primary_key_column = call_user_func(array($class, 'primaryKeyName'), $class);
        $table = call_user_func(array($class, 'tableName'), $class);

        $in = "('".implode("', '", $primary_keys)."')";

        // get table entries
        $cmd = "SELECT
                    *
                FROM
                    `".$table."`
                WHERE
                    `".$primary_key_column."` IN ".$in."
                ORDER BY
                    CASE
                    ";
        foreach($primary_keys as $i => $v)
        {
            $cmd .= " WHEN ".$primary_key_column." = ".$v." THEN ".$i." ";
        }

        $cmd .= " END";

        // USE PDO to build the objects of the proper class

        $STH = $GLOBALS['DBH']->prepare($cmd);
        $STH->setFetchMode(PDO::FETCH_CLASS, $class);
        $STH->execute();

        while($row = $STH->fetch(PDO::FETCH_CLASS))
        {
            $list[] = $row;
        }

        return $list;
    }

    // These methods are used to map the table names and class names for the ORM logic
    protected static function primaryKeyName($class)
    {
        return call_user_func(array($class, 'underscoreFromCamelCase'), $class) . '_id';
    }

    protected static function tableName($class)
    {
        return DB_T_PREPEND . call_user_func(array($class, 'underscoreFromCamelCase'), $class);
    }

    protected static function underscoreFromCamelCase($str)
    {
        $str[0] = strtolower($str[0]);
        $func = create_function('$c', 'return "_" . strtolower($c[1]);');

        return preg_replace_callback('/([A-Z])/', $func, $str);
    }
}

This class will perform ORM operations on an instance and can be used very simply in the following ways.

Lets extend this class with a normal entity class like Fruit.

Entity Class Creation:


class Fruit extends AxisEntityObject
{

}

Simple!

Create:


$fruit = new Fruit();
$fruit->fruit_name = 'Pineapple';
$fruit_id = $fruit->Create(); 

Retrieve and Update:


$fruit = Fruit::Factory('21'); // 21 is our primary key id in the database.

Combine retrieval with updating for simple object updating.


$fruit = Fruit::Factory('21');
$fruit->name = 'Orange';
$fruit->Update();

Delete:


$fruit = Fruit::Factory('21'); // 21 is our primary key id in the database.
$fruit->Delete();

Case Study: Museum Works Audio Tours

Museum Works Audio Tours is a company that provides multimedia presentations to captivate and interest museum visitors for exhibits of all types. Much of their presentation includes audio messages and ambient sounds to represent the exhibit to users of the museum.

Museum Works Audio Tours contracted Angelwire Creative to build a media management back end system for their existing web site. I was brought in to design and develop the tools they needed a management system that allowed them to upload audio files of type mp3, wav, or aiff. They as well needed to correlate a visual image with each audio file for their viewers. The management system needed to present the files of their choosing to the visitors of their site and present them with a UI tool for navigating the audio files each with their own cover image.

This was incorporated for them through a back end management system that is still being used today.

Technologies Employed:

  • PHP
  • Quicktime Player
  • PHP GD Library

Case Study: Xeta Telecommunications

Xeta Telecommunications was in need of an eCommerce solution customized to their own specific business model. This meant an eCommerce store that allowed for a tier based pricing and allocating users into particular pricing brackets. It also meant a private web store that only specific clientele could access. All of this was needed and it was needed quickly and within a limited budget. I was contracted through Angelwire Creative to help achieve this goal.

To accommodate this project a customized version of osCommerce was employed that would involve a heavy modification of the core application. Since osCommerce was an open source project, it allowed myself to make the needed changes to the code base for Xeta, while at the same time eliminating the need for developing all the other necessary eCommerce tools such as user management, product management, shopping carts, and payment APIs.

Strike Iron

Portions of the eCommerce store leveraged the powerful API provided by Strike Iron at www.strikeiron.com. Their API allowed for customer zip code look-ups to determine the user’s zip code based upon their designated street address, and also resolve possible city conflictions regarding zip codes that overlap zip code boundaries. It also allowed for validating customer addresses, phone numbers, and was used to calculated tax rates based on customer location.

Technologies Employed:

Case Study: Nations Home Warranty

Nations Home Warranty is a Texas based home warranty company servicing native Texans and Oklahomans with outstanding home warranty services. Much of their great customer interaction is supported by a well-built and maintained application process on their website home-warranty.com.

The Nations Home Warranty was in a serious need up upgrade and contemporizing to capture their visitors attention and provide them with tools for managing their home warranty contacts. I was employed through Angelwire Creative to redesign an existing but dated application process that had become un-maintainable by their staff. Much of the PHP was designed for PHP 3 and needed a serious redesign if it were to survive a single server update.

The application process was not only streamlined for PHP 4 at the time, it was also rebuilt to be modular with adjustable pricing and pricing packages for their various contracts. It also was integrated into a user toolkit that users could use to review their existing contracts and update their contact information. As well, the application process was built to service individuals who spoke both English and Spanish as a first language.

This was made possible by refactoring the site’s text into manageable language files that could be swapped out as the user defined their language. It would allow for the expansion of any number of languages as well.

An application management center was developed for the Nations Home Warranty staff to review new applications, archive or remove outdated applications, and contact their customer base. As well, support was given for the staff to issue newsletters to their subscribed email base.

 


 

Case Study: The Voice of Pro Equine Group

The Voice of Pro Equine group was a project developed for the internal use of Pro Equine Group of Dallas, TX. I was contracted to develop this application through Angelwire Creative. It accomplished many goals that the company needed to complete and is in active use today.

The Problem:

Pro Equine Group was a parent company over several smaller companies that sold and managed products regarding equestrian and riding uses. Each of these smaller companies managed and oversaw their own repository of customer contacts for use in future marketing and sales information. The problem lay in the fact that many of these customers purchased products among several of their child companies at each time, and since each child company oversaw their own database of customer information, linking and managing the customers was an impossibility.

Each child company would store their customer database using a different format and different structure. Some stored their information use Excel, others a database. All stored them differently.

The Solution:

I developed a system for managing not only their child companies, but also created a powerful series of tools that alleviated the separated customer databases. A powerful upload tool allowed their user base to upload any CSV of any format containing customers and any other secondary customer information into a single database that Pro Equine Group could manage and use for marketing.

This was accomplished by developing a front-end tool that allowed users of a specific user type the power to create custom columns in a report table within a single database. They could designate each of these columns to contain a different type of data and provide a related description and name. These columns represented other customer information such as receipt information, age ranges etc.

In correlation to this, a powerful upload tool allowed them to map any columns in a CSV to the columns defined in the database table. These mappings could be saved for re-use by the user and could be shared across the chile company to which that user belonged.

Uploading the CSV against this mapping would send the information through a filtering system that would recognize duplicated customers within the upload and would either mark that entry as “to merge”, “conflicted” or “matched”. Each option resulted in a protocol that could allow administrators to handle the confictions or merging process.

In addition to the upload procedure, a tool was developed to present a Graphical User Interface to the user that would allow them to shape and create their own queries into the uploaded customer database. The results would print in an HTML tabular format with support for downloading into Excel. These reports could be saved in the form of “campaigns” that would be re-usable and re-reportable by users for marketing usage or anything desired.

These tools allowed Pro Equine group the ability to overcome their separated customer database problems with ease. But alongside of this solution a fully functioning user management, customer search tool, and company management tool allowed Pro Equine group to really manage their business better than before.

Technologies Employed:

  • PHP & MySQL
  • CSV parsing
  • PHP Mailer
  • Microsoft Excel
  • JavaScript & jQuery

Case Study: nSiteBuilders.com

nSiteBuilders is an active project under the ownership of Suarez & Simmons Inc. The website, nSiteBuilders.com, is an online service package designed for home builder companies of all sizes. The purpose of the application is to bring tools to the home builder that allow for accurately tracking and reporting upon competitor data.

Some of the types of data captured are:
  • Geographic location of the competitor selling efforts in relation to any company selling effort.
  • A consolidated database of home builders across the nation.
  • Competitor “shop” information such as: pricing and sales, cancellations, lots remaining and more.
  • Historical records for all data that can be easily perused through a single timeline slider.
In addition to competitor tracking tools, the application includes a powerhouse of internal company tools that allow each customer to modify the application to suit their particular business model and hierarchy. These tools include:
  • The ability to model the business hierarchy and subsequent users by any means, be it departmental, geographic or anything.
  • The ability to control the security access for every single user under your account and define what operations are permitted and those that are not permitted.
  • The ability to brand the application to fit not only your corporate image, but even define the brands for sub division brands if desired.

Lets take a look at the various technologies employed to power nSiteBuilders.com.

jQuery was extensively utilized for large portion of the application’s interface. Features such as:
  • Nested Sortable drag and drop hierarchies.
  • Tree based file view systems.
  • Ajax supported sliders for navigating the application timeline without refreshing the page.

 

 


Google Maps API was leveraged for a large part of the application’s toolset. Innovative uses for the Google Maps system were implemented for uses such as:
  • Geocoding of all home builder selling efforts and communities.
  • Reverse address lookup  for any location worldwide.
  • Competitor reporting based on geographical location and distance calculations.

 

 


Powerful usage of the PHP GD Library allows for user administrators to brand the nSiteBuilders application with their own logos.
  • To accommodate this, PHP manipulates the cache settings of the browser to reload the cache of the branded logo on the fly for all relative users to ensure that the new logos are applied immediately.
  • The ability to change the CSS of the application’s interfaces lets the tool feel more identifiable with the client’s staff.
  • And multiple brands can be applied to a single account specific to the client’s builder brands and their employees.

 

 


Other technologies employed:

VIEW MORE CASE STUDIES