Page 1 of 1

Autogenerated Model

Posted: Tue Apr 13, 2010 2:19 pm
by Benjamin
As some of you know, I'm a big fan of the Symfony framework. I've decided that it's not in my best interest to use it however, due to the time involved in constantly looking things up and working around the constraints it imposes. The extra time involved defeats the purpose of using a framework to speed up the development process.

So yet again, I've decided to role my own framework. I am currently working on code which will auto generate model classes from a database schema. Posted below is what the generator will currently create by itself. I intend to add auto generated validation (within the constraints of the MySQL field itself), table joins support and some other things I haven't thought of yet.

This is modelled after the models created by Symfony, so it may look a bit familiar. Anyway, any insight would be appreciated. Here is a class generated from the phpbb_icons table.

Code: Select all

<?php
/**
 * Base class that represents a row from the 'phpbb_icons' table.
 *
 * This class was autogenerated.
 *
 * Tue April 13th 14:07:27 2010
 *
 * @package     forum
 */

abstract class basePhpbbIcons {
    /** The primary key for this table */
    const PRIMARY_KEY = 'icons_id';

    /** The MySQL table name */
    const TABLE_NAME = 'phpbb_icons';

    /** The MySQL database name */
    const DATABASE_NAME = 'forum';

    /**
     * Container for field values
     * @var     array
     */
    protected $_values;

    /**
     * Container for default field values
     * @var     array
     */
    protected $_defaultValues = array(
        'IconsId'          => NULL,
        'IconsUrl'         => NULL,
        'IconsWidth'       => '0',
        'IconsHeight'      => '0',
        'IconsOrder'       => '0',
        'DisplayOnPosting' => '1',
    );

    /**
     * Maps standardized field names to MySQL field names
     * @var     array
     */
    private static $_fieldMap = array(
        'IconsId'          => 'icons_id',
        'IconsUrl'         => 'icons_url',
        'IconsWidth'       => 'icons_width',
        'IconsHeight'      => 'icons_height',
        'IconsOrder'       => 'icons_order',
        'DisplayOnPosting' => 'display_on_posting',
    );

    /**
     * Container for tracking modified fields
     * @var     array
     */
    protected $_changedColumns = array();

    /**
     * Container for tracking object state
     * @var     array
     */
    protected $_attributes = array(
        'is_deleted'   => false,
        'is_validated' => false,
        'is_saved'     => false,
        'is_new'       => true,
    );

    public function __construct() {
        $this->kernel = kernel::get_instance();

        $this->_setDefaults();
    }

    /**
     * Get the [icons_id] column value.
     *
     * @return    mediumint(8) unsigned
     */
    public function getIconsId() {
        return $this->_values['icons_id'];
    }

    /**
     * Get the [icons_url] column value.
     *
     * @return    varchar(255)
     */
    public function getIconsUrl() {
        return $this->_values['icons_url'];
    }

    /**
     * Get the [icons_width] column value.
     *
     * @return    tinyint(4)
     */
    public function getIconsWidth() {
        return $this->_values['icons_width'];
    }

    /**
     * Get the [icons_height] column value.
     *
     * @return    tinyint(4)
     */
    public function getIconsHeight() {
        return $this->_values['icons_height'];
    }

    /**
     * Get the [icons_order] column value.
     *
     * @return    mediumint(8) unsigned
     */
    public function getIconsOrder() {
        return $this->_values['icons_order'];
    }

    /**
     * Get the [display_on_posting] column value.
     *
     * @return    tinyint(1) unsigned
     */
    public function getDisplayOnPosting() {
        return $this->_values['display_on_posting'];
    }

    /**
     * Set the value of [icons_id] column.
     *
     * @param      mediumint(8) unsigned $v new value
     * @return     PhpbbIcons The current object (for fluent API support)
     */
    public function setIconsId($v) {
        if ($this->_values['icons_id'] !== $v) {
            $this->_values['icons_id'] = $v;
            $this->updateModified('icons_id');
        }

        return $this;
    }

    /**
     * Set the value of [icons_url] column.
     *
     * @param      varchar(255) $v new value
     * @return     PhpbbIcons The current object (for fluent API support)
     */
    public function setIconsUrl($v) {
        if ($this->_values['icons_url'] !== $v) {
            $this->_values['icons_url'] = $v;
            $this->updateModified('icons_url');
        }

        return $this;
    }

    /**
     * Set the value of [icons_width] column.
     *
     * @param      tinyint(4) $v new value
     * @return     PhpbbIcons The current object (for fluent API support)
     */
    public function setIconsWidth($v) {
        if ($this->_values['icons_width'] !== $v) {
            $this->_values['icons_width'] = $v;
            $this->updateModified('icons_width');
        }

        return $this;
    }

    /**
     * Set the value of [icons_height] column.
     *
     * @param      tinyint(4) $v new value
     * @return     PhpbbIcons The current object (for fluent API support)
     */
    public function setIconsHeight($v) {
        if ($this->_values['icons_height'] !== $v) {
            $this->_values['icons_height'] = $v;
            $this->updateModified('icons_height');
        }

        return $this;
    }

    /**
     * Set the value of [icons_order] column.
     *
     * @param      mediumint(8) unsigned $v new value
     * @return     PhpbbIcons The current object (for fluent API support)
     */
    public function setIconsOrder($v) {
        if ($this->_values['icons_order'] !== $v) {
            $this->_values['icons_order'] = $v;
            $this->updateModified('icons_order');
        }

        return $this;
    }

    /**
     * Set the value of [display_on_posting] column.
     *
     * @param      tinyint(1) unsigned $v new value
     * @return     PhpbbIcons The current object (for fluent API support)
     */
    public function setDisplayOnPosting($v) {
        if ($this->_values['display_on_posting'] !== $v) {
            $this->_values['display_on_posting'] = $v;
            $this->updateModified('display_on_posting');
        }

        return $this;
    }

    /**
     * Sets all values to their defaults
     *
     * @return     PhpbbIcons The current object (for fluent API support)
     */
    public function _setDefaults() {
        $this->_values = $this->_defaultValues;
        return $this;
    }

    /**
     * Deletes the current object from the database
     *
     * @param      object $dbCon A database connection
     * @return     bool True on success, False on failure
     */
    public function doDelete($dbCon = null) {
        if ($this->_attributes['is_deleted']) {
            return true;
        }

        if ($this->_attributes['is_new']) {
            return true;
        }

        if (self::PRIMARY_KEY == '') {
            trigger_error("'" . self::TABLE_NAME . "' does not have a primary key.", E_USER_ERROR);
            return false;
        }

        if (empty($dbCon) || !is_object($dbCon)) {
            $dbCon = basePhpbbIcons::_getDBCon();
        }

        if ($dbCon->query("DELETE FROM " . self::DATABASE_NAME . '.' . self::TABLE_NAME . " WHERE " . self::TABLE_NAME . "." . self::PRIMARY_KEY . " = '" . $dbCon->escape($this->_values[self::PRIMARY_KEY]) . "' LIMIT 1")) {
            $this->_setAttributes(array(
              'is_deleted'     => true,
            ));

            return true;
        }

        return false;
    }

    /**
     * Saves the current object.  The object will either be inserted or updated
     * depending on its source. Objects populated from a database query will be
     * updated.
     *
     * @param      object $dbCon A database connection
     * @return     bool True on success, False on failure
     */
    public function doSave($dbCon = null) {
        if ($this->_attributes['is_deleted']) {
            trigger_error("doSave() Deleted objects cannot be saved.", E_USER_ERROR);
            return false;
        }

        if ($this->_attributes['is_saved']) {
            return true;
        }

        if (self::PRIMARY_KEY == '') {
            trigger_error("'" . self::TABLE_NAME . "' does not have a primary key.", E_USER_ERROR);
            return false;
        }

        if (empty($dbCon) || !is_object($dbCon)) {
            $dbCon = basePhpbbIcons::_getDBCon();
        }

        if ($this->_attributes['is_new']) {
            $data = $this->_values;
            unset($data[self::PRIMARY_KEY]);
            if ($dbCon->insert($data, self::DATABASE_NAME . '.' . self::TABLE_NAME)) {
		        $this->_setAttributes(array(
		          'is_saved'     => true,
		          'is_new'       => false,
		        ));

		        $this->_values[self::PRIMARY_KEY] = $dbCon->insert_id();
		        $this->changedColumns = array();
		        return true;
            } else {
                return false;
            }
        } else {
            $query = "UPDATE " . self::DATABASE_NAME . '.' . self::TABLE_NAME . " SET";

            foreach ($this->changedColumns as $columnName => $isChanged) {
                if (!$isChanged) continue;
                $value = ($this->_values[$columnName] === NULL) ? "NULL" : "'" . $dbCon->escape($this->_values[$columnName]) . "'";
                $query .= " " . self::TABLE_NAME . ".$columnName = $value,";
            }

            $query = rtrim($query, ",");

            $query .= " WHERE " . self::TABLE_NAME . "." . self::PRIMARY_KEY . " = '" . $dbCon->escape($this->_values[self::PRIMARY_KEY]) . "' LIMIT 1";

            if ($dbCon->query($query)) {
                $this->_setAttributes(array(
                  'is_saved'     => true,
                ));

                $this->changedColumns = array();
                return true;
            } else {
                return false;
            }
        }
    }

    /**
     * Flag updated columns
     *
     * @param      string $fieldName The modified field name
     * @return     PhpbbIcons The current object (for fluent API support)
     */
    protected function updateModified($fieldName) {
        $this->changedColumns[$fieldName] = 'true';

        $this->_attributes['is_validated'] = false;
        $this->_attributes['is_saved'] = false;

        return $this;
    }


    /**
     * Sets the attributes for the current object
     *
     * @param      array $atr the attributes to set
     * @return     PhpbbIcons The current object (for fluent API support)
     */
    protected function _setAttributes($atr) {
        if (!is_array($atr) || count($atr) < 1) {
            trigger_error("_setAttributes() First argument must be an array", E_USER_ERROR);
        }

        foreach ($atr as $k => $v) {
            if (!isset($this->_attributes[$k])) {
               trigger_error("_setAttributes() $k does not exist as an attribute.", E_USER_ERROR);
               continue;
            }

            $this->_attributes[$k] = $v;
        }
        return $this;
    }

    /**
     * Retrieve a single object by pkey.
     *
     * @param      int $key the primary key.
     * @return     $PhpbbIcons
     */
    public static function retrieveByPK($key, $dbCon = null) {
        if (self::PRIMARY_KEY == '') {
            trigger_error("'" . self::TABLE_NAME . "' does not have a primary key.", E_USER_ERROR);
            return false;
        }

        if (empty($dbCon) || !is_object($dbCon)) {
            $dbCon = basePhpbbIcons::_getDBCon();
        }

        return basePhpbbIcons::doSelectOne("SELECT * FROM " . self::DATABASE_NAME . '.' . self::TABLE_NAME . " WHERE " . self::TABLE_NAME . '.' . self::PRIMARY_KEY . "= '" . $dbCon->escape($key) . "'");
    }

    public static function _getDBCon() {
        $kernel = kernel::get_instance();
        return $kernel->db;
    }

    /**
     * Retrieve a single object with MySQL query
     *
     * @param      string $query the database query
     * @param      object $dbCon the database connection
     * @return     $PhpbbIcons
     */
    public static function doSelectOne($query, $dbCon = null) {
        if (empty($dbCon) || !is_object($dbCon)) {
            $dbCon = basePhpbbIcons::_getDBCon();
        }

        if (false === $n = $dbCon->turbo_row($query)) {
            return false;
        }

        $PhpbbIcons = new PhpbbIcons();
        $PhpbbIcons->_values = $n;
        $PhpbbIcons->_setAttributes(array(
          'is_deleted'   => false,
          'is_validated' => true,
          'is_saved'     => true,
          'is_new'       => false,
        ));
        return $PhpbbIcons;
    }
}

Re: Autogenerated Model

Posted: Tue Apr 13, 2010 2:29 pm
by AbraCadaver
I haven't had time to look through it, but you might have a look at the CakePHP bake scripts. Even if you don't intend to use Cake, most of the auto code generation code should be usable with modifications to suite your framework. I also remember there being templates to generate the M/V/C files. The bake script supports joins, validation etc. and it's all PHP for your enjoyment :-) Might save some time. Also, I haven't looked at it in a while, but the Qcodo framework had a fairly good code generator. Cake uses naming conventions such as table and field names to work out the relations. Qcodo used the primary/foreign keys defined in the database tables to work out the relations.

Re: Autogenerated Model

Posted: Tue Apr 13, 2010 2:36 pm
by Benjamin
AbraCadaver wrote:I haven't had time to look through it, but you might have a look at the CakePHP bake scripts. Even if you don't intend to use Cake, most of the auto code generation code should be usable with modifications to suite your framework. I also remember there being templates to generate the M/V/C files. The bake script supports joins, validation etc. and it's all PHP for your enjoyment :-) Might save some time. Also, I haven't looked at it in a while, but the Qcodo framework had a fairly good code generator. Cake uses naming conventions such as table and field names to work out the relations. Qcodo used the primary/foreign keys defined in the database tables to work out the relations.
Yeah, that's not a bad idea. This has only taken me about 8 hours or so to write so far and I feel like I'm more than halfway done. I don't think it will take me over an hour or two to add support for joins. The data validation might take a bit longer. So I don't know if I would save any time by pulling apart another framework.

Re: Autogenerated Model

Posted: Tue Apr 13, 2010 6:50 pm
by Christopher
The two things that jump out at me are:

1. You could use __get() and __set() for all your field getters/setters are greatly reduce the code, and code generation needs.

2. Your naming is not very standard for either ActiveRecord or TableDataGateway. I think just save(), findOne(), findAll(), findByPK(), etc. would be more standard.

Re: Autogenerated Model

Posted: Wed Apr 14, 2010 3:57 am
by Weirdan
Christopher wrote:2. Your naming is not very standard for either ActiveRecord or TableDataGateway. I think just save(), findOne(), findAll(), findByPK(), etc. would be more standard.
Depends on your background. For example, retrieveByPK is something I immediately recognize (having experience with Propel), and find*() are not.

Benjamin, are there any specific reasons to not reuse the Propel library?

Re: Autogenerated Model

Posted: Wed Apr 14, 2010 11:15 am
by Benjamin
Weirdan wrote:Benjamin, are there any specific reasons to not reuse the Propel library?
I'm wanting a solution that will create these from an existing schema, rather than yaml files.
I want to integrate this into a custom framework.
I don't want to use the database abstraction layer.

Re: Autogenerated Model

Posted: Wed Apr 28, 2010 7:00 am
by Weirdan
Benjamin wrote:I'm wanting a solution that will create these from an existing schema, rather than yaml files.
Propel can do that: propel-gen ./ reverse
Benjamin wrote:I want to integrate this into a custom framework.
Integrating Propel into ZF was quite easy... this, of course, depends on how much integration you want.
Benjamin wrote:I don't want to use the database abstraction layer.
Ok, this rules Propel out since it uses PDO.

Re: Autogenerated Model

Posted: Sat May 01, 2010 6:04 pm
by phu
Have you considered going at it from the other direction? I've always been a fan of automatically building the schema based on the code; that way you can stick to writing PHP and let the framework (for the most part) handle the database setup.

Re: Autogenerated Model

Posted: Mon May 03, 2010 12:53 pm
by Benjamin
phu wrote:I've always been a fan of automatically building the schema based on the code;
How would that work? Given a large enough data set you could determine the appropriate and most efficient data type for each field programmatically, but otherwise without implicitly specifying them directly the code would be guessing. Also, since writing code takes longer than creating a DB schema, how would that save time?

Re: Autogenerated Model

Posted: Tue May 11, 2010 12:19 pm
by phu
Benjamin wrote:How would that work? Given a large enough data set you could determine the appropriate and most efficient data type for each field programmatically, but otherwise without implicitly specifying them directly the code would be guessing. Also, since writing code takes longer than creating a DB schema, how would that save time?
Based on the code, not the data. Obviously you wouldn't want to try to guess based on the data it processes; using a simple string specification as the default for your classes (and nulling the values on instantiation), though, would be a simple and effective way to handle type definition that could be used when generating tables. It would also provide the groundwork for implicit field validation.

I've linked to a framework on github that illustrates one (more involved) way to handle this; it'd be simpler but less flexible to just infer types based on defaults, e.g. turning array members into joined relations in the schema based on their names (which I have also done to good effect, but wanted more flexibility with naming and type handling).