Working with the ORM Installer

One of the more tedious elements of any application is managing model object classes. Fortunately, the Istarel Workshop Application Framework has a command line ORM tool that automatically creates (or updates) model objects based on the database schema.

It has a number of switches and options to fine-tune any updates. The simplest version of the command:

cd ~/Sites/fw/install
php orm_install -n Istarel Workshop -w /Users/markf/Sites -a iw -r article

This tells the installer to create the model object classes for the article relation in the database used by the iw application. It uses [u]/Users/markf/Sites/iw/conf/ApplicationConstants.php[/u] to connect to the database and retrieve the schema information for the article relation (table). The default parent model object classes are created or updated: [u]/rsrc/model/default/DefaultArticle.php and /rsrc/model/factory/DefaultArticleFactory.php[/u]. The child model object classes, which are the classes developers should use, are only created if not already present (Article is a subclass of DefaultArticle, and ArticleFactory is a subclass of DefaultArticleFactory).

Listing: DefaultArticle.php

class DefaultArticle extends ORMObject
{
    // Properties representing table columns
    public $article_id;
    public $author_id;
    public $title;
    public $publish_date;
    public $ready_to_publish;
    public $public_path;
    public $article;
    public $content;

// Factory

    function factoryClass()
    {
        return 'ArticleFactory';
    }

// Many Relations

    function articleCategories($requestor = null)
    {
        $select = new ORMSelect('article_category');
        $select->addCriteria(new ORMCriterion('article_id', $this->article_id));
        return ArticleCategoryFactory::factory()->retrieveObjects($requestor,
            'ArticleCategory', $select);
    }

// Validation

    function validateAuthorId(&$err)
    {
        if ($this->invalidForRule('IWRequiredRule', $this->author_id))
            $err[] = IWDatabase::ERROR_IS_NULL;
        return empty($err);
    }

    function validateTitle(&$err)
    {
        if ($this->invalidForRule('IWRequiredRule', $this->title))
            $err[] = IWDatabase::ERROR_IS_NULL;
        if ($this->invalidForRule('IWLengthRule', $this->title, [0, 100]))
            $err[] = IWDatabase::ERROR_TOO_LONG;
        return empty($err);
    }

    function validatePublishDate(&$err)
    {
        if ($this->invalidForRule('IWRequiredRule', $this->publish_date))
            $err[] = IWDatabase::ERROR_IS_NULL;
        return empty($err);
    }

    function validatePublicPath(&$err)
    {
        if ($this->invalidForRule('IWLengthRule', $this->public_path, [0, 200]))
            $err[] = IWDatabase::ERROR_TOO_LONG;
        if (! $this->verifyPropertyIsUnique('public_path'))
            $err[] = IWDatabase::ERROR_NOT_UNIQUE;
        return empty($err);
    }

    function validateArticle(&$err)
    {
        if ($this->invalidForRule('IWRequiredRule', $this->article))
            $err[] = IWDatabase::ERROR_IS_NULL;
        return empty($err);
    }
}

Listing: DefaultArticleFactory.php

class DefaultArticleFactory extends ORMFactory
{
    function factory()
    {
        return IWFactory::defaultFactory('ArticleFactory');
    }

    function retrieveArticle($property_value = null, $property_name = null,
                            $properties = ORMObject::ALL_PROPERTIES)
    {
        return ORMFactory::retrieveObject('Article', $property_value, $property_name, $properties);
    }

    function articles($requestor = null)
    {
        return ArticleFactory::factory()->retrieveObjects($requestor, 'Article');
    }

    function instanceClass()
    {
        return 'Article';
    }

    function sourceName()
    {
        return 'article';
    }

    function identifierProperty()
    {
        return 'article_id';
    }

    function primaryKeySequence()
    {
        return 'article_seq';
    }

    function propertyType()
    {
        return array(
            'article_id'       => ORMPostgreSQLAdapter::PGSQL_INT4,
            'author_id'        => ORMPostgreSQLAdapter::PGSQL_INT4,
            'title'            => ORMPostgreSQLAdapter::PGSQL_VARCHAR,
            'publish_date'     => ORMPostgreSQLAdapter::PGSQL_DATE,
            'ready_to_publish' => ORMPostgreSQLAdapter::PGSQL_BOOLEAN,
            'public_path'      => ORMPostgreSQLAdapter::PGSQL_VARCHAR,
            'article'          => ORMPostgreSQLAdapter::PGSQL_TEXT,
            'content'          => ORMPostgreSQLAdapter::PGSQL_TEXT
        );
    }

    function noCopyProperties()
    {
        return array('article_id');
    }
}

I left off comment blocks at the top of each file which define the class (DefaultArticle or DefaultArticleFactory) and project (Istarel Workshop).

Note that the automatically generated validation methods apply any constraints defined in the database schema. For example, there is a unique constraint on the public_path column of the article table, so that is reflected in the validatePublicPath() method. Likewise, any varchar columns have a IWLengthRule applied in their validation method.

Accessor Methods

If you want to have accessor methods automatically written, you include the -p or --with-property-methods switch.

cd ~/Sites/fw/install
php orm_install -p -n Istarel Workshop -w /Users/markf/Sites -a iw -r article

This creates accessor pairs which look like:

function setTitle($title)
    {
        $this->setValueForProperty('title', $title);
        return $this;
    }

    function title()
    {
        return $this->title;
    }

Using setTitle($title) or setValueForProperty('title', $title) ensures that the model object knows that it has changed such that when its save() method is invoked, an UPDATE statement gets called. If you directly reference $article->title to change the property value, the model object will not be aware it changed. Why use direct reference at all? It's a more efficient approach when you are updating properties in a situation where you know you will not be saving those changes (like when preparing data for a list in an editor).

I always use this switch in my applications.

Deletion Control

If you want a delete() method call to properly delete downstream objects (as is often the case when there is a foreign key relation to the current table), you can use the -d or --use-deletion-control switch.

cd ~/Sites/fw/install
php orm_install -d -p -n Istarel Workshop -w /Users/markf/Sites -a iw -r article

The deletion control works by creating a willDelete() method that applies the recipe design pattern to manage deletion of the individual related table records. For article (which has a to-many relationship to article_category) the deletion control section is:

function willDelete()
    {
        $this->deleteArticleCategories();
    }

    function deleteArticleCategories()
    {
        $delete = new ORMDelete('article_category');
        $delete->addCriteria(new ORMCriterion('article_id', $this->article_id));
        $delete->execute();
    }

By using the recipe design pattern, the developer model object class (Article) can override willDelete() to replace the entire deletion mechanism, or override the individual deletion methods to apply application-specific needs only to part of the standardized deletion process.

I often use this switch in my applications, but not for every table.

Multiple Tables

The ORM installer is designed for convenience. You can target multiple tables: simply space separate the tables you want to update.

cd ~/Sites/fw/install
php orm_install -d -p -n Istarel Workshop -w /Users/markf/Sites -a iw -r article category

You can target all the database tables using the -t or --with-tables-only switch.

cd ~/Sites/fw/install
php orm_install -d -p -t -n Istarel Workshop -w /Users/markf/Sites -a iw
php orm_install -d -p -n Istarel Workshop -w /Users/markf/Sites -a iw --with-tables-only

Future articles will discuss specific model object needs beyond the scope of the ORM installer.