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:
+ *
+ * - session: data persisting within a single user session.
+ * - state persister: data persisting through all requests/sessions (e.g. hit counter).
+ * - cache: volatile and fast storage. It may be used as storage medium for session or state persister.
+ *
+ *
+ * @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);
+ }
+}