Automatic creation of tables/columns that dont exist using ActiveRecord

Hopefully this code comes in handy to someone. It’s a starting point for something I was working on, and figured it might be useful to someone else. It’s pretty basic, and could use a lot of improvement. It’s purpose is to speed up development time when working with ActiveRecord. It automatically creates tables that don’t exist, and creates/modifies column types based on the information being passed. The current datatypes are pretty basic, only supporting int, varchar, and text, although it should be pretty easy to expand on them.

Right now when comparing data types, its hardcoded to make sure text columns don’t get downgraded to varchars when setting data. In the future I might try to create a little better system for not downgrading data types. This would allow better support for adding types of tinyint, bigint, bigtext, etc. There is also no escaping of passed table/column names, so that should be added at some point to prevent issues when using weird table/column names, as well as improve security.

It has a public $freeze variable that when set to true, won’t modify the database structure. You will have to manage your indexes yourself, and every table has an id column that is set as primary index and auto-increments. For the most part, this code seems to work fine during my testing, although I had only used it for a small project that didn’t see the light of day.

To use it, just extend your models from ExtendedActiveRecord instead of CActiveRecord.

Hopefully someone finds this useful, or expands upon the idea.

Note: this code should not be used in a production environment, and make sure you have database backups, just in case :)


<?php


/**

 * ExtendedActiveRecord extends CActiveRecord to create/modify column types on the fly.

 *

 */

class ExtendedActiveRecord extends CActiveRecord

{

	/**

	 * @var bool Used to freeze the database structure on production servers

	 */

	public $freeze = false;


	/**

	 * PHP setter magic method.

	 * This method is overridden so that column types can be created/modified on the fly.

	 * @param string property name

	 * @param mixed property value

	 */

	public function __set($name,$value)

	{

		if($this->setAttribute($name,$value)===false)

		{

			if(isset($this->getMetaData()->relations[$name]))

				$this->_related[$name]=$value;

			else

			{

				if ($this->freeze===false)

				{

					$command=$this->getDbConnection()->createCommand('ALTER TABLE `'.$this->tableName().'` ADD `'.$name.'` '.self::getDbType($value).' NOT NULL');

					$command->execute();

					$this->getDbConnection()->getSchema()->refresh();

					$this->refreshMetaData();

				}

				$this->__set($name, $value);

			}

		}

		elseif ($this->freeze===false)

		{			

			$cols = $this->getDbConnection()->getSchema()->getTable($this->tableName())->columns;

			if ($name != 'id' && strcasecmp($cols[$name]->dbType, self::getDbType($value)) != 0)

			{

				//prevent TEXT from being downgraded to VARCHAR

				if (!(strcasecmp($cols[$name]->dbType,'TEXT')==0 && strcasecmp(self::getDbType($value),'VARCHAR(255)')==0))

				{

					$command=$this->getDbConnection()->createCommand('ALTER TABLE  `'.$this->tableName().'` CHANGE  `'.$name.'`  `'.$name.'` '.self::getDbType($value));

					$command->execute();

					$this->getDbConnection()->getSchema()->refresh();

					$this->refreshMetaData();

					$this->setAttribute($name,$value);

				}

			}

		}

	}

	

	/**

	 * Returns a columns datatype based on the value passed.

	 * @param mixed property value

	 */

	public static function getDbType($value)

	{

		if (is_numeric($value) && floor($value)==$value)

			return 'INT(11)';

		

		if (is_numeric($value))

			return 'DOUBLE';

		

		if (strlen($value) <= 255)

			return 'VARCHAR(255)';

		

		return 'TEXT';

	}

	

	/**

	 * Returns the static model of the specified AR class.

	 * The model returned is a static instance of the AR class.

	 * It is provided for invoking class-level methods (something similar to static class methods.)

	 *

	 * EVERY derived AR class must override this method as follows,

	 * <pre>

	 * public static function model($className=__CLASS__)

	 * {

	 *     return parent::model($className);

	 * }

	 * </pre>

	 *

	 * @param string active record class name.

	 * @return CActiveRecord active record model instance.

	 */

	public static function model($className=__CLASS__)

	{

		if(isset(self::$_models[$className]))

			return self::$_models[$className];

		else

		{

			$model=self::$_models[$className]=new $className(null);

			$model->attachBehaviors($model->behaviors());

			$model->_md=new ExtendedActiveRecordMetaData($model);

			return $model;

		}

	}


	/**

	 * @return CActiveRecordMetaData the meta for this AR class.

	 */

	public function getMetaData()

	{

		if($this->_md!==null)

			return $this->_md;

		else

			return $this->_md=self::model(get_class($this))->_md;

	}

}


/**

 * ExtendedActiveRecordMetaData is extended from CActiveRecordMetaData.  

 * It's modified to create tables that don't exist.

 */

class ExtendedActiveRecordMetaData Extends CActiveRecordMetaData

{

	/**

	 * Constructor.

	 * @param CActiveRecord the model instance

	 */

	public function __construct($model)

	{

		$tableName=$model->tableName();

		if(($table=$model->getDbConnection()->getSchema()->getTable($tableName))===null) {

			if ($model->freeze===false)

			{

				$command=$model->getDbConnection()->createCommand('CREATE TABLE  `'.$tableName.'` (`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY) ENGINE = MYISAM ;');

				$command->execute();

			

				$model->getDbConnection()->getSchema()->refresh();			

				$table=$model->getDbConnection()->getSchema()->getTable($tableName);

			}

			else

			{

				throw new CDbException(Yii::t('yii','The table "{table}" for active record class "{class}" cannot be found in the database.',

				array('{class}'=>get_class($model),'{table}'=>$tableName)));

			}

		}

		return parent::__construct($model);

	}

}

The idea of this sounds very good. Have you already taken a look at the CAdvancedArBehavior, a small

extension from me, that adds some additional features to the ActiveRecord?

http://www.yiiframework.com/extension/cadvancedarbehavior/

I would be glad to implement your functionality with your contributed codebase into this behavior.

The ‘freeze’ should be renamed to something like ‘dynamic’ being deactivated by default. Only when

i know what i am doing and i want my database to be changed by my AR models, i set this to true.

This extension needs a update anyway, so i would integrate your code.

What do you think?