[extension]Settings

This forum topic is related to http://www.yiiframework.com/extension/settings/


This extension is an alternative to my "myconfig" extension from here: http://www.yiiframework.com/extension/myconfig/ but it uses only the database+caching.

As explained in this comment http://www.yiiframework.com/extension/myconfig/#c3727

using this extension has some advantages and some extra requirements.

##Requirements

  1. Yii 1.1.x

  2. A Cache component activated (CFileCache will do it just fine)

  3. Your DB component should have the $tablePrefix property defined, even if it is empty:


 

'db'=>array(  

[...]  

'tablePrefix' => '',//or fill it with your own prefix.  

 [...]  

),



##Instalation

This extension is designed for performance mostly, so things like automatically create the database table, or having the table name dynamically set in the extension configuration are out of discussion.

  1. Create a database table, named settings:

 

CREATE TABLE IF NOT EXISTS `settings` (

      `id` int(11) NOT NULL auto_increment,

      `category` varchar(64) NOT NULL default 'system',

      `key` varchar(255) NOT NULL,

      `value` text NOT NULL,

      PRIMARY KEY  (`id`),

      KEY `category_key` (`category`,`key`)

    ) ENGINE=InnoDB  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ; 



  1. Add the component to the main.php config file:

 

[...]

'cache'=>array(

            'class'=>'system.caching.CFileCache',

        ),

'settings'=>array(

            'class'     => 'CmsSettings',

            'cacheId'   => 'global_website_settings',

            'cacheTime' => 84000,

        ),

[...]



##Usage


 

/*   

* Set a database item:  

* $itemName can be an associative array() in key=>value pairs  ($itemValue="" in this case) 

*/

Yii::app()->settings->set($categoryName, $itemName, $itemValue, $toDatabase=true);  


// Get a database item:  

Yii::app()->settings->get($categoryName, $itemName);  


// Get all items from a category:  

Yii::app()->settings->get($categoryName);


// Delete a database item:  

Yii::app()->settings->delete($categoryName, $itemName);  


// Delete all items from a category:  

Yii::app()->settings->delete($categoryName);  


//Import items from a file:  

$items=include('path/to/file.php');//returns an array() in key=>value pairs  

Yii::app()->settings->set($categoryName, $items);  




The component uses something like "lazy loading" for loading items within a category, meaning that the items from a category will be loaded when you request them first time. Beside this, at the end of the request, the items from that requested category are written to cache, so next time when you request them, they will be served from cache.

Note, the component is smart enough to know itself when a new item/category has been added and refresh the cache accordingly so you don’t have to keep track of the items you add to database.

Basically, in most of the cases, the database will be hit only once, then all items will be served from cache, which means you will get a nice way to manage the project configuration without performance penalties.

##Notes & Changelog

Version 1.1.c

This version has been improved and it is recommended to update your existing component with this one. The code is fully compatible, so all you have to do is to replace the old component with this new one and everything will work just fine.

Version 1.1.d

-> Contains small performance improvements.

-> You can now use the get() method like


 

$retrieve_custom_settings=Yii::app()->settings->get('system',array('admin_email','contact_email','my_email'=>'some default value'));



In the above example, $retrieve_custom_settings becomes an array having the ‘admin_email’ and ‘contact_email’ keys. If these values are empty or they don’t exists in database then they will be set to null otherwise you will retrieve their values. It is set this way so that you can safely use $retrieve_custom_settings[‘admin_email’] even if it doesn’t exists.

-> Now, this is more like a feature that is disabled by default.

Because some of you don’t preffer


 

Yii::app()->settings->set('catName',$array_of_items);  

//or  

Yii::app()->settings->get('catName',$array_of_items);  



i am experimenting a new way of setting/getting items into categories.

Let’s assume we need a category named “movies”, with the new feature, we can set/get items like:


 

Yii::app()->settings->setMovies($array_of_items);  

//or  

Yii::app()->settings->getMovies($array_of_items);  

Yii::app()->settings->getMovies('some_key','some_default_value');  



In case you need this feature enabled, uncomment the __call() method on line 165 and please provide feedback on using the extension this way.

Thanks

here is a modified version, like I posted in the comments

I’ve been using EConfig from Y!! for ‘ages’.

That’s a good alternative because it is using binary fields and php serialize.

But this extension has a category field…

Anything else in favor of this config extension?

@Gustavo - thanks for adding this, i hope today i can make time for a review and for the changes you’ve suggested. I will let you know when you can check it.

@jacmoe - well, i remember that before i write this extension, i took a look to the other one you specified and it didn’t work as i needed.

However, i believe my extension is more practical and a bit smarter(it uses cache, it separates by categories, it lazy loads categories, it knows when to add/delete to/from cache etc etc).

Maybe you can give it a try yourself and see the difference, having in mind that you use the other one for a long time ?

@Gustavo, i attached the new version with the changes you purposed.

I slightly changed the variable names but the functionality remains the same as you wanted.

More than this, i also added the option to auto create the database table and set the db engine of your choice.

Basically, the available settings are:




'settings'=>array(

        'class'                 => 'CmsSettings',

	'cacheComponentId'	=> 'cache',

        'cacheId'   		=> 'global_website_settings',

        'cacheTime' 		=> 60*60*24*365,

	'tableName'		=> '{{settings}}',

	'dbComponentId'		=> 'db',

	'createTable'		=> false,

	'dbEngine'		=> 'InnoDB',

        ),



Please give it a try (i did some small tests as i haven’t had enough time yet) and let me know if there is any bug after these changes.

here a version

I added a documentation to the common public methods that you might want to rewrite and a few changes that you might want to consider, like

public $_ methods are now private forcing the use of setters/getters methods

on setTable method, I don’t see the need to require ‘{{’ and ‘}}’

the setDbEngine method is restricting the db to be mysql, I changed it to allow different db engines

The createTable option is a nice improvement.

btw, great job on this extension.

Thanks so much for everything, you did a really great job with all the changes :)

Here is some explanation:

If we define




private $_cacheComponentId='cache';

private $_cacheId='global_website_settings';

private $_cacheTime=0;


private $_dbComponentId='db';

private $_tableName='{{settings}}';

private $_createTable=false;

private $_dbEngine='InnoDB';



Say for example you have a class which extends this one and you want to overwrite the setTableName() method, well in this case, adding something like this:




class ECmsSettings extends CmsSettings

{


    public function init()

    {

        parent::init();

	echo $this->getTableName();

	exit;

    }

	


	public function setTableName($name)

	{

		$this->_tableName=12345;//just for testing purposes, keep it simple...

	}

	

	public function getTableName()

	{

		return $this->_tableName;

	}

	


    

}



Will result in a "Property "ECmsSettings._tableName" is not defined." error, because the property is private, accessible for the parent class only.

That’s the reason i will let them protected, to avoid this kind of error.

Next, is the setTableName() method, just wanted you to know that i defined it like:




public function setTableName($name)

{

	if($this->getCreateTable()&&(strpos($name, '{{')!=0||strpos($name, '}}')!=(strlen($name)-2)))

		throw new CException('The table name must be like "{{'.$name.'}}" not just "'.$name.'"');

	$this->_tableName=$name;

}



So that in case a user defines the params like:




'settings'=>array(

'class'     		=> 'CmsSettings',

'cacheComponentId'	=> 'cache',

'cacheId'   		=> 'global_website_settings',

'cacheTime' 		=> 60*60*24*365,

'tableName'		=> 'settings',//Attention here....

'dbComponentId'		=> 'db',

'createTable'		=> false,

'dbEngine'		=> 'InnoDB',

),



The component will trigger an error letting the user know that he needs to append the {{ and }} before/after table name.

For the rest of the changes i have nothing to comment, everything looks great to me.

I attached the last revision of the class, take a look and see if you want to change anything else.

Also, since you tested this component allot, do you think i should keep the __call() method (allowing something like getMovies($itemName) instead of get(‘movies’, $itemName) ), or it’s just too fancy and we shouldn’t bother with this ?

Thanks!

I’m glad to contribute

I see and you are right, protected is better

As for the setTableName method I still don’t see the need to require {{ }} when creating because you will replace by ‘’ and add the db tablePrefix always

Here a new version, I didn’t modify the functionality, I just added more documentation to methods and fixed some typos in my previous doc.

I don’t know if you noticed, I changed a bit this method, calling first the parent implementation then calling your implementation

I don’t use it, but I see as a nice option.

For those who uses this extension, this might help you:

Its a base model to collect user input

It adds a method save($runValidation=true,$attributes=null) to CFormModel and an abstract method ‘categoryName’ that must return the category that the model will collect the input

use it like this




<?php

class UserSettings extends SettingsModel{

	public $captcha=true;

	public $sendActivationMail=true;

	public $loginNotActivated=true;

	public $autoLogin=true;

	function rules(){

    	return array(

        	array('captcha, sendActivationMail, loginNotActivated, autoLogin', 'boolean'),

    	);

	}

	function attributeLabels(){

    	return array(

        	'captcha'=>t('Use captcha'),

        	'sendActivationMail'=>t('Send verification email'),

        	'loginNotActivated'=>t('Needs email verified to perform login'),

        	'autoLogin'=>t('Auto login after registration'),

    	);

	}

	function categoryName(){

    	return 'user';

	}

}



use it like




$model=new UserSettings;

if(isset($_POST['UserSettings'])){

	$model->attributes=$_POST['UserSettings'];

	if($model->save())

    	//do something

}

$this->render('...');



It’s not just for create(the create is the instance when we don’t need the {{ }} ), i mean, we later use $this->getTableName() to query the database table, and in that instance we need the {{ and }}, makes sense now ?

Regarding the __call() method, i am still in doubt, will see if i keep or remove it before i will add this extension to the repository, meanwhile i will download it, check the latest changes, and in the end upload it to the repository.

Thanks for all your time :)

@twisted,

thank you for the great work,

are you planning on adding a user interface to manage the settings?

do you have any suggestions or hints on making it?

In my admin panel i divided the settings area in tabs, each tab being a category.

Using chtml i am creating inputs like $_POST[‘category_name’][‘setting_key’] then when the form is saved, save the settings for each category. This is nothing fancy, but it is simple and works just fine for me :)

It is good enough,

I think it will be very nice add to your great extension.

I, at least, will appreciate it if you share it with me :)

Here is the overview if it helps you:

CONTROLLER:




<?php


class SettingsController extends CmsBackendController

{

    /**

	 * This is the default 'index' action that is invoked

	 * when an action is not explicitly requested by users.

	 */

	public function actionIndex()

	{

	   $settings=Yii::app()->settings;

       

	   if(isset($_POST['Settings']))

           {

              $_POST['Settings']=Yii::app()->input->stripClean($_POST['Settings']);

              foreach($_POST['Settings'] AS $category=>$items)

              {

                $settings->set($category, $items);

              }

           }

	   

	   $this->setViewData(compact('settings'));

	   

       $this->render('index', $this->getViewData());

    }


}



VIEW:




$this->breadcrumbs=array(

	Yii::t('AdminModule.admin','Settings'),

);

?>


<div class="g12">

    <h1><?php echo Yii::t('app','Global website settings');?></h1>

    <p><?php echo Yii::t('app', 'Please don\'t play here unless you really know what you are doing.');?></p>

    

    <?php echo CHtml::beginForm('','post',array('id'=>'settings-form', 'autocomplete'=>'off'));?>

    

    <?php

    $this->widget('zii.widgets.jui.CJuiTabs', array(

        'id'=>'settingsTabs',

        'tabs'=>array(

           Yii::t('app','SEO')=>array('content'=>$this->renderPartial('tabs/seo', $this->getViewData(), true), 'id'=>'tab1'),

           Yii::t('app','Cookies')=>array('content'=>$this->renderPartial('tabs/cookies', $this->getViewData(), true), 'id'=>'tab2'),

           Yii::t('app','Email')=>array('content'=>$this->renderPartial('tabs/email', $this->getViewData(), true), 'id'=>'tab3'),

           Yii::t('app','Social Media')=>array('content'=>$this->renderPartial('tabs/social_media', $this->getViewData(), true), 'id'=>'tab4'),

        ),

        'options'=>array(

            'collapsible'=>false,

            'fx'=>array(

                'opacity'=> 'toggle',

    			'duration'=> 'fast',

    			'height'=>'auto'

            ),

        ),

        'htmlOptions'=>array('style'=>'float:left;width:100%'),

    ));

    ?>

    

    <div class="clear h10">&nbsp;</div>

    <fieldset>

    <section>

        <button type="submit"><?php echo Yii::t('app','Save changes'); ?></button>

        <button type="reset" onclick="window.location.href='<?php echo Yii::app()->createUrl('admin/index');?>'"><?php echo Yii::t('app','Cancel changes and return to list'); ?></button>

    </section>

    </fieldset>

    <?php echo CHtml::endForm(); ?>

</div>



And this is a partial view for one of the settings tab (category), namely the SEO tab:





<fieldset>

	<label><?php echo Yii::t('app', 'Seo settings');?></label>

	<section>

        <?php echo CHtml::label(Yii::t('app','Meta title'),'Settings_seo_title',array('class'=>'desc')); ?>

        <div>

            <?php echo CHtml::textArea('Settings[seo][title]',$settings->get('seo','title'),array('rows'=>6)); ?>  

        </div>

	</section>

    <section>

        <?php echo CHtml::label(Yii::t('app','Meta keywords'),'Settings_seo_keywords',array('class'=>'desc')); ?>

        <div>

            <?php echo CHtml::textArea('Settings[seo][keywords]',$settings->get('seo','keywords'),array('rows'=>6)); ?>

        </div>

	</section>

    <section>

        <?php echo CHtml::label(Yii::t('app','Meta description'),'Settings_seo_description',array('class'=>'desc')); ?>

        <div>

            <?php echo CHtml::textArea('Settings[seo][description]',$settings->get('seo','description'),array('rows'=>6)); ?>    

        </div>

	</section>

</fieldset>




Hope it helps :)

@twisted,

thank you very much for sharing that,

why do you extend from CmsBackendController and what does it do?


class SettingsController extends CmsBackendController

and what does this method do?




$this->setViewData(compact('settings'));

I’m sorry I’m new to programming and usually ask stupid questions :)

Well, your question is correct.

I use a CmsBaseController as the base controller where i put methods that all the others controllers will inherit.

Take a look:




<?php


class CmsBaseController extends CController

{

	/**

	 * @var string the default layout for the controller view. Defaults to '//layouts/column1',

	 * meaning using a single column layout. See 'protected/views/layouts/column1.php'.

	 */

	public $layout='//layouts/main';

	/**

	 * @var array context menu items. This property will be assigned to {@link CMenu::items}.

	 */

	public $menu=array();

    /**

	 * @var array context menu items. This property will be assigned to {@link CMenu::items}.

     * Used mostly for modules.

	 */

	public $menuItems=array();

	/**

	 * @var array the breadcrumbs of the current page. The value of this property will

	 * be assigned to {@link CBreadcrumbs::links}. Please refer to {@link CBreadcrumbs::links}

	 * for more details on how to specify this property.

	 */

	public $breadcrumbs=array();

	/**

	* @var string the keywords of the current page.

	**/

    public $pageKeywords;

	/**

	* @var string the description of the current page.

	**/

    public $pageDescription;

	/**

	* @var title the keywords of the current page.

	**/

    public $pageTitle;

	/**

	* @var array view data. This property is populated from withing controllers with 

	* variables that should be available in the views and partial views.

	**/

	private $_viewData = array();

	

    public function init()

    {

        parent::init();

        Yii::app()->clientScript->registerCoreScript('jquery');

    }


	/**

	* CmsBaseController::setViewData()

	* @param mixed $key

	* @param mixed $value

	* @return current context

	*

	* This is the setter method for $_viewData array.

	* Usually it needs to be called in the controller to set the view variables.

	**/

	public function setViewData($key, $value='')

	{

		if(is_array($key))

			$this->_viewData = CMap::mergeArray($this->_viewData, $key);

		else

			$this->_viewData[$key] = $value;

		

		return $this;

	}

	/**

	* CmsBaseController::getViewData()

	* @param mixed $key

	* @return mixed the array key if exists or the entire data array.

	*

	* This is the getter method for $_viewData array.

	**/

	public function getViewData($key='')

	{

		if(empty($key))

			return $this->_viewData;

		

		return isset($this->_viewData[$key]) ? $this->_viewData[$key] : null;

	}

    

}



Now, this CmsBaseController sets some default variables and methods that will be available for me in every other controller that extend this one.

The CmsBackendController is a special controller that is extended by all the controllers from the admin panel.

In this controller i have methods and variables only for backend:




<?php

class CmsBackendController extends CmsBaseController

{

    public $pageInnerTitle ;

    public $pageInnerSubtitle ;


    public function init()

    {

        parent::init();


        if(!Yii::app()->user->checkAccess('administrator'))

        {

            $this->redirect(Yii::app()->createUrl('admin/login')) ;

        }


        Yii::app()->theme='acp' ;

        Yii::app()->clientScript->registerCoreScript('jquery.ui');

        Yii::app()->clientScript->registerScriptFile(Yii::app()->theme->baseUrl.'/js/functions.js');

        Yii::app()->clientScript->registerScriptFile(Yii::app()->theme->baseUrl.'/js/theme/plugins.js');

        Yii::app()->clientScript->registerScriptFile(Yii::app()->theme->baseUrl.'/js/theme/wl_Dialog.js');

        Yii::app()->clientScript->registerScriptFile(Yii::app()->theme->baseUrl.'/js/theme/wl_Alert.js');

    }


}




As you will see, the CmsBaseController just sets the variables, but this controller, CmsBackendController sets the theme for the admin panel, also it registers some scripts for use with the admin panel.

There is a similar CmsFrontendController that will set the theme and scripts/variables for frontend.

Basically using this approach, having a BaseController and specialized controllers for each main section of the website offers huge advantages.

The methods setViewData and getViewData are methods for convenience to pass the variables from controllers to views.

The main reason i use them, is that when you have a huge controller and you need to pass many variables to the view you need to do smth like:




$this->render('index', array(

   'variable' => $variable,

   'other' => $other,

   'etc'  => $etc

));



And these variables will be available on your view, BUT if your view loads many partial views, then you will need to pass these variables to the partial view also, resulting in a huge amount of repetitive code, therefore, in these cases if the variables are set with setViewData() i can retrieve them all in any view with getViewData(). Makes sense ?

The compact() function is php native function: www.php.net/compact.

Let me know if you have other questions:)

There is something wrong with 1.2 delete.

It does clear from the memory but doesn’t delete from the database.

I’ve tried this from a console application.





echo Yii::app()->settings->get('system', 'email_help_form_email_to');  

        echo "\r\n";

        Yii::app()->settings->delete('system', 'email_help_form_email_to');  

        Yii::app()->settings->deleteCache();

        echo Yii::app()->settings->get('system', 'email_help_form_email_to');  

        echo "\r\n";

        exit;



Thanks for pointing this out, i will check as soon as possible and get back to you.

Meanwhile, does this issue appears on a browser app ?

I can’t check that out, we are developing console apps.

@twisted1919

your extension and your answers here are all very useful to me. thank you