diff --git a/CHANGELOG b/CHANGELOG index 8c8674ee35..8b130cb5e5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -28,6 +28,7 @@ Version 1.1.17 work in progress - Enh #3686: Wrapper div of hidden fields in CForm now have style `display:none` instead of `visibility:hidden` to not affect the layout (cebe, alaabadran) - Enh #3738: It is now possible to override the value of 'required' html option in `CHtml::activeLabelEx()` (matteosistisette) - Enh #3827: Added the $options parameter to be used in stream_socket_client in CRedisCache. +- Enh #3872: Added database-based StatePersister implementation (AloneCoder) - Enh #3948: Enhanced CHttpRequest path info extraction for compatibility with PHP 7 (dmitrivereshchagin) - Enh #3949: Added `$overwriteAll` argument to `CConsoleCommand::copyFiles()` which can be set to true in noninteractive commands (dmitrivereshchagin) - Chg #3776: Autoloader now doesn't error in case of non-existing namespaced classes so other autoloaders have chance to handle these (alexandernst) diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 080dbb90e3..15c4b30252 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -652,6 +652,7 @@ public static function registerAutoloader($callback, $append=false) 'CApplicationComponent' => '/base/CApplicationComponent.php', 'CBehavior' => '/base/CBehavior.php', 'CComponent' => '/base/CComponent.php', + 'CDbStatePersister' => '/base/CDbStatePersister.php', 'CErrorEvent' => '/base/CErrorEvent.php', 'CErrorHandler' => '/base/CErrorHandler.php', 'CException' => '/base/CException.php', diff --git a/framework/base/CDbStatePersister.php b/framework/base/CDbStatePersister.php new file mode 100644 index 0000000000..2f2779af14 --- /dev/null +++ b/framework/base/CDbStatePersister.php @@ -0,0 +1,153 @@ + + * @link http://www.yiiframework.com/ + * @copyright 2008-2013 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @package system.base + * @since 1.1.17 + */ + +/** + * CDbStatePersister implements a database persistent data storage. + * + * It can be used to keep data available through multiple requests and sessions. + * + * By default, CDbStatePersister stores data in a table named 'state'. + * You may change the location by setting the {@link stateTableName} property. + * + * To retrieve the data from CDbStatePersister, call {@link load()}. To save the data, + * call {@link save()}. + * + * Comparison among state persister, session and cache is as follows: + * + * + * @package system.base + * @since 1.1.17 + */ +class CDbStatePersister extends CApplicationComponent implements IStatePersister +{ + /** + * @var string the database table name storing the state data. Make sure the table + * exists or database user is granted to CREATE tables. + */ + public $stateTableName='state'; + /** + * @var string connection ID + */ + public $dbComponent='db'; + /** + * @var CDbConnection instance + */ + public $db; + + /** + * @var string Column name for value-field + */ + public $valueField='value'; + /** + * @var string Column name for key-field + */ + public $keyField='key'; + + /** + * Initializes the component. + * This method overrides the parent implementation by making sure {@link stateFile} + * contains valid value. + */ + public function init() + { + parent::init(); + if($this->stateTableName===null) + throw new CException(Yii::t('yii', 'stateTableName param cannot be null.')); + $this->db=Yii::app()->getComponent($this->dbComponent); + if($this->db===null) + throw new CException(Yii::t('yii', '\'{db}\' component doesn\'t exist.',array( + '{db}'=>$this->dbComponent + ))); + if(!($this->db instanceof CDbConnection)) + throw new CException(Yii::t ('yii', '\'{db}\' component is not a valid CDbConnection instance.',array( + '{db}'=>$this->dbComponent + ))); + if($this->db->schema->getTable($this->stateTableName,true)===null) + $this->createTable(); + } + + /** + * Loads state data from persistent storage. + * @return mixed state data. Null if no state data available. + */ + public function load() + { + $command=$this->db->createCommand(); + $command=$command->select($this->valueField)->from($this->stateTableName); + $command=$command->where($this->db->quoteColumnName($this->keyField).'=:key',array( + ':key'=>Yii::app()->name + )); + $state=$command->queryScalar(); + if(false!==$state) + return unserialize($state); + else + return null; + } + + /** + * Saves application state in persistent storage. + * @param mixed $state state data (must be serializable). + * @return int + */ + public function save($state) + { + $command=$this->db->createCommand(); + if(false===$this->exists()) + return $command->insert($this->stateTableName,array( + $this->keyField=>Yii::app()->name, + $this->valueField=>serialize($state) + )); + else + return $command->update($this->stateTableName,array($this->valueField=>serialize($state)), + $this->db->quoteColumnName($this->keyField).'=:key', + array(':key'=>Yii::app()->name) + ); + } + + /** + * @return mixed + */ + public function exists() + { + $command=$this->db->createCommand(); + $command=$command->select($this->keyField)->from($this->stateTableName); + $command=$command->where($this->db->quoteColumnName($this->keyField).'=:key',array( + ':key'=>Yii::app()->name + )); + return $command->queryScalar(); + } + + /** + * Creates state persister table + * @throws CException + */ + protected function createTable() + { + try + { + $command=$this->db->createCommand(); + $command->createTable($this->stateTableName,array( + $this->keyField=>'string NOT NULL', + $this->valueField=>'text NOT NULL', + 'PRIMARY KEY ('.$this->db->quoteColumnName($this->keyField).')' + )); + } + catch (CDbException $e) + { + throw new CException(Yii::t('yii','Can\'t create state persister table. Check CREATE privilege for \'{db}\' connection user or create table manually with SQL: {sql}.',array('{db}'=>$this->dbComponent,'{sql}'=>$command->text ) ) ); + } + } +} diff --git a/framework/yiilite.php b/framework/yiilite.php index cc6be4dc6a..8157a0b484 100644 --- a/framework/yiilite.php +++ b/framework/yiilite.php @@ -361,6 +361,7 @@ public static function registerAutoloader($callback, $append=false) 'CApplicationComponent' => '/base/CApplicationComponent.php', 'CBehavior' => '/base/CBehavior.php', 'CComponent' => '/base/CComponent.php', + 'CDbStatePersister' => '/base/CDbStatePersister.php', 'CErrorEvent' => '/base/CErrorEvent.php', 'CErrorHandler' => '/base/CErrorHandler.php', 'CException' => '/base/CException.php', diff --git a/tests/framework/base/CDbStatePersisterTest.php b/tests/framework/base/CDbStatePersisterTest.php new file mode 100644 index 0000000000..7b00966f3f --- /dev/null +++ b/tests/framework/base/CDbStatePersisterTest.php @@ -0,0 +1,37 @@ +markTestSkipped('PDO and SQLite extensions are required.'); + // clean up runtime directory + $app=new TestApplication; + $app->reset(); + } + + public function testLoadSave() + { + $app=new TestApplication(array( + 'components'=>array( + 'db'=>array( + 'class' => 'CDbConnection', + 'connectionString' => 'mysql:host=127.0.0.1;port=3306;dbname=yii', + 'username' => 'test', + 'password' => 'test', + 'emulatePrepare' => true, + 'charset' => 'utf8', + 'enableParamLogging' => true, + ), + 'statePersister' => array( + 'class' => 'CDbStatePersister' + ) + ) + )); + $sp=$app->statePersister; + $data=array('123','456','a'=>443); + $sp->save($data); + $this->assertEquals($sp->load(),$data); + } +}