Simple configuration file editor for Yii

    Good evening, Habrahabr.

    Today I will talk about a small component of the form that I happened to write for the wonderful PHP framework Yii. This component (or rather, the form model) allows you to edit config files directly from the web. The article was inspired by a recent post on similar functionality, but that implementation is based on the database. This is not entirely inactive for Yii configuration files. In addition, you will have to pay extra requests for the database / cache for such a decision, and you need to save them in projects with high traffic.

    There will be a lot of code in the article, but I will try to split it into logical pieces.

    Idea


    The configuration file in Yii is a regular php script that returns an array.
    For instance:
    return array(
        'name' => 'My Awesome Web Site',
        'lang' => 'ru',
        'sourceLang' => 'en',
    );
    


    The configuration sometimes indicates some static site parameters that change once a year or do not change at all. For example, take the administrator’s email address or phone number, which is displayed next to the logo. These parameters definitely need to be allowed to be edited by the site administrator, but do not let him crawl into the code, right? (:

    Implementation


    The implementation itself is quite simple, but at the same time confusing. I will try to sort things out.

    Model

    A model is data. And what data does the config file have? Right, an array of configuration. And for him we need to create a model.

    class ConfigForm extends CFormModel
    {
    	/** @var array Массив, содержащий в себе всю конфигурацию */
    	private $_config = array();
    	/**
    	 * Инициализация модели
    	 * @param array $config Массив из конфига
    	 * @param string $scenario Сценарий валидации
    	 */
    	public function __construct($config = array(), $scenario = '')
    	{
    		parent::__construct($scenario);
    		$this->setConfig($config);
    	}
    	public function setConfig($config)
    	{
    		$this->_config = $config;
    	}
    	public function getConfig()
    	{
    		return $this->_config;
    	}
    }
    

    So far so simple, right?
    In fact, there is no need for the privacy of the $ _config variable, but it will not be superfluous if you suddenly want to change the rules of the game

    Next, we need to establish the rules by which the attribute names will be formed. You do not want to add a new field to the model each time (although, nevertheless, you have to add something, but more on that later). So, let's say we have such a configuration array:
    array(
    	'name' => 'My Awesome Site', // на самом деле, изменять имя сайта - плохая идея
    	'params' => array(
    		'adminEmail' => 'admin@example.com',
    		'phoneNumber' => '555-555-555',
    		'motto' => 'the best of the most awesome',
    	),
    );
    


    From this array you need to get the following attributes: name , params [adminEmail] , params [phoneNumber] , params [motto] . Accordingly, you need to do this recursively, and here is my solution:

    My decision
    	/**
    	 * Возвращает все атрибуты с их значениями
    	 * 
    	 * @return array
    	 */
    	public function getAttributes()
    	{
    		$this->attributesRecursive($this->_config, $output);
    		return $output;
    	}
    	/**
    	 * Возвращает имена всех атрибутов
    	 * 
    	 * @return array
    	 */
    	public function attributeNames()
    	{
    		$this->attributesRecursive($this->_config, $output);
    		return array_keys($output);
    	}
    	/**
    	 * Рекурсивно собирает атрибуты из конфига
    	 *
    	 * @param array $config
    	 * @param array $output
    	 * @param string $name
    	 */
    	public function attributesRecursive($config, &$output = array(), $name = '')
    	{
    		foreach ($config as $key => $attribute) {
    			if ($name == '')
    				$paramName = $key;
    			else
    				$paramName = $name . "[{$key}]";
    			if (is_array($attribute))
    				$this->attributesRecursive($attribute, $output, $paramName);
    			else
    				$output[$paramName] = $attribute;
    		}
    	}
    


    At the output, we get the desired array, which would be nice to create validation rules:
    	public function rules()
    	{
    		$rules = array();
    		$attributes = array_keys($this->_config);
    		$rules[] = array(implode(', ', $attributes), 'safe');
    		return $rules;
    	}
    

    Everything is simple here. In order for attributes of the form params [motto] to be considered safe, it is sufficient to make only the parent attribute safe.
    It should be understood that in fact you can write anything to the params array, but adding an additional root attribute to the config will not work. I can try to explain this point in the comments if you have questions.

    To have direct access to these attributes through the expression $ model -> $ attribute, we extend the __set () and __get () methods:
    	public function __get($name)
    	{
    		// Если атрибут есть в конфиге - возвращаем его. Если нет - передаём эстафетную палочку родительскому классу
    		if (isset($this->_config[$name]))
    			return $this->_config[$name];
    		else
    			return parent::__get($name);
    	}
    	public function __set($name, $value)
    	{
    		// Если атрибут есть в конфиге - пишем в него
    		if (isset($this->_config[$name]))
    			$this->_config[$name] = $value;
    		else
    			parent::__set($name, $value);
    	}
    

    What could be easier?

    So, the model framework is ready. Now she knows how to say what attributes she has and eat attributes from the form. To test this model, you can write the usual action to process the POST request and build the form:
    	public function run()
    	{
    		$path = YiiBase::getPathOfAlias('application.config') . '/params.php';
    		$model = new ConfigForm(require($path));
    		if (isset($_POST['ConfigForm'])) {
    			$model->setAttributes($_POST['ConfigForm']);
    			if($model->save($path))
    			{
    				Yii::app()->user->setFlash('success config', 'Конфигурация сохранена');
    				$this->controller->refresh();
    			}
    		}
    		$this->controller->render('config', compact('model'));
    	}
    

    This action is given as an example, it is not necessary to copy it completely, just grasp the essence

    of something? Does CFormModel have no save () method? True, but we will write it here to save the result to a file. As in the construction of attributes, here we need recursion:

    Save to file
    	public function save($path)
    	{
    		$config = $this->generateConfigFile();
    		// Предупредим программиста о том, что в файл не получится записать
    		if(!is_writable($path))
    			throw new CException("Cannot write to config file!");
    		file_put_contents($path, $config, FILE_TEXT);
    		return true;
    	}
    	public function generateConfigFile()
    	{
    		$this->generateConfigFileRecursive($this->_config, $output);
    		$output = preg_replace('#,$\n#s', '', $output); // Регулярка делает красиво
    		return " $value) {
    			if (!is_array($value))
    				$output .= str_repeat("\t", $depth) . "'" . $this->escape($attribute) . "' => '" . $this->escape($value) . "',\n";
    			else {
    				$output .= str_repeat("\t", $depth) . "'" . $this->escape($attribute) . "' => ";
    				$this->generateConfigFileRecursive($value, $output, $depth + 1);
    			}
    		}
    		$output .= str_repeat("\t", $depth - 1) . "),\n"; // Глубина нужна, чтобы определить длину отступа
    	}
    	private function escape($value)
    	{
    		/**
    		 * Это для того, чтобы с кавычкой не сломался синтаксис (php-injection).
    		 * Не исключаю, что в php есть какой-нибудь специальный метод, 
    		 * зато я знаю, что ничего лишнего заэкранировано не будет
    		*/
    		return str_replace("'", "\'", $value);
    	}
    


    The good KeepYourMind suggested in a comment that you can use the php function var_export () for generation , which I did not know about before writing this bike generator

    . We also need a View file, in which the form itself is generated by existing attributes.
    beginWidget('CActiveForm', array(
    	'id' => 'config-form',
    	'enableAjaxValidation' => false, // Ajax- и Client- валидацию я не предусматривал, т.к. это не имеет смысла
    	'enableClientValidation' => false,
    ));
    foreach ($model->attributeNames() as $attribute) {
    	echo CHtml::openTag('div', array('class' => 'row'));
    	{
    		echo $form->labelEx($model, $attribute);
    		echo $form->textField($model, $attribute);
    	}
    	echo CHtml::closeTag('div');
    }
    echo CHtml::submitButton('Сохранить');
    $this->endWidget();
    

    In order for us to have beautiful captions for the attributes, we will have to firmly define them in the model.
    	public function attributeLabels()
    	{
    		return array(
    			'name' => 'Название сайта',
    			'params[adminEmail]' => 'Email администратора',
    			'params[phoneNumber]' => 'Номер телефона',
    			'params[motto]' => 'Девиз сайта',
    		);
    	}
    

    This is ugly and rude, however, I did not find another normal way to do this. You can put them in an additional file, but it will not change the essence - anyway, to add an option you will have to edit 2 files.

    That's basically it. I give the full model code without detailed comments:

    Full model code
    class ConfigForm extends CFormModel
    {
    	private $_config = array();
    	/**
    	 * Инициализация модели
    	 * @param array $config Массив из конфига
    	 * @param string $scenario Сценарий валидации
    	 */
    	public function __construct($config = array(), $scenario = '')
    	{
    		parent::__construct($scenario);
    		$this->setConfig($config);
    	}
    	public function setConfig($config)
    	{
    		$this->_config = $config;
    	}
    	public function getConfig()
    	{
    		return $this->_config;
    	}
    	public function __get($name)
    	{
    		if (isset($this->_config[$name]))
    			return $this->_config[$name];
    		else
    			return parent::__get($name);
    	}
    	public function __set($name, $value)
    	{
    		if (isset($this->_config[$name]))
    			$this->_config[$name] = $value;
    		else
    			parent::__set($name, $value);
    	}
    	public function save($path)
    	{
    		$config = $this->generateConfigFile();
    		if(!is_writable($path))
    			throw new CException("Cannot write to config file!");
    		file_put_contents($path, $config, FILE_TEXT);
    		return true;
    	}
    	public function generateConfigFile()
    	{
    		$this->generateConfigFileRecursive($this->_config, $output);
    		$output = preg_replace('#,$\n#s', '', $output);
    		return " $value) {
    			if (!is_array($value))
    				$output .= str_repeat("\t", $depth) . "'" . $this->escape($attribute) . "' => '" . $this->escape($value) . "',\n";
    			else {
    				$output .= str_repeat("\t", $depth) . "'" . $this->escape($attribute) . "' => ";
    				$this->generateConfigFileRecursive($value, $output, $depth + 1);
    			}
    		}
    		$output .= str_repeat("\t", $depth - 1) . "),\n";
    	}
    	private function escape($value)
    	{
    		return str_replace("'", "\'", $value);
    	}
    	/**
    	 * Возвращает все атрибуты с их значениями
    	 *
    	 * @return array
    	 */
    	public function getAttributes()
    	{
    		$this->attributesRecursive($this->_config, $output);
    		return $output;
    	}
    	/**
    	 * Возвращает имена всех атрибутов
    	 *
    	 * @return array
    	 */
    	public function attributeNames()
    	{
    		$this->attributesRecursive($this->_config, $output);
    		return array_keys($output);
    	}
    	/**
    	 * Рекурсивно собирает атрибуты из конфига
    	 *
    	 * @param array $config
    	 * @param array $output
    	 * @param string $name
    	 */
    	public function attributesRecursive($config, &$output = array(), $name = '')
    	{
    		foreach ($config as $key => $attribute) {
    			if ($name == '')
    				$paramName = $key;
    			else
    				$paramName = $name . "[{$key}]";
    			if (is_array($attribute))
    				$this->attributesRecursive($attribute, $output, $paramName);
    			else
    				$output[$paramName] = $attribute;
    		}
    	}
    	public function attributeLabels()
    	{
    		return array(
    			'name' => 'Название сайта',
    			'params[adminEmail]' => 'Email администратора',
    			'params[phoneNumber]' => 'Номер телефона',
    			'params[motto]' => 'Девиз сайта',
    		);
    	}
    	public function rules()
    	{
    		$rules = array();
    		$attributes = array_keys($this->_config);
    		$rules[] = array(implode(', ', $attributes), 'safe');
    		return $rules;
    	}
    }
    


    Errors and typos, please report via private messages. I apologize for typos in advance - for a long time I wrote nothing but code in such volumes

    Only registered users can participate in the survey. Please come in.

    Do you use a similar configurator in the admin panel?

    • 14.8% Yes 23
    • 60.6% No 94
    • 24.5% Now I will be 38

    Also popular now: