⬆️ ⬇️

Simple configuration file editor for Yii

Good evening, Habrahabr.



Today I will talk about a small form component that I have written 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 is inspired by a recent post about similar functionality, but that implementation is based on a DB. This is not entirely valid for the Yii configuration files. In addition, for such a decision you will have to pay extra requests to the database / cache, and you need to save them in projects with high attendance.



The article will have a lot of code, but I will try to divide it into logical pieces.



Idea



The configuration file in Yii is a regular php script that returns an array.

For example:

return array( 'name' => 'My Awesome Web Site', 'lang' => 'ru', 'sourceLang' => 'en', ); 


')

The configuration sometimes indicates some static parameters of the site, which change once a year or do not change at all. For example, take the administrator's E-mail or phone number that appears next to the logo. These parameters definitely need to be allowed to edit the site administrator, but do not let him climb into the code, right? (:



Implementation



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



Model


A model is data. And what data have config files? Correct, array configuration. Here 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; } } 


While everything is simple, is not it?

In fact, there is no need for privacy of the $ _config variable, but this will not be superfluous if you suddenly want to change the rules of the game.



Next, we need to establish the rules by which attribute names will be formed. You do not want to add a new field to the model each time (although, nevertheless, you will have to add something, but more on that later). So, let's say we have this configuration array:

 array( 'name' => 'My Awesome Site', //   ,    -   'params' => array( 'adminEmail' => 'admin@example.com', 'phoneNumber' => '555-555-555', 'motto' => 'the best of the most awesome', ), ); 




The following attributes should be obtained from this array: name , params [adminEmail] , params [phoneNumber] , params [motto] . Accordingly, this should be done 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 required array, to which it would be nice to create validation rules:

  public function rules() { $rules = array(); $attributes = array_keys($this->_config); $rules[] = array(implode(', ', $attributes), 'safe'); return $rules; } 


It's simple. In order for params [motto] type attributes to be considered safe, it suffices to make only the parent attribute safe.

It should be understood that in fact you can write anything into 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 any questions arise.



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 frame model is ready. Now she is able to say what attributes she has and eat attributes from the form. To test this model, you can write a simple action to handle 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 for example, you should not copy it completely, just enough to grasp the essence



Something? Does the save () method have a CFormModel? True, but we will write it here to save the result to a file. As in the construction of attributes here, we will 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 "<?php\nreturn " . $output . ";\n"; } public function generateConfigFileRecursive($attributes, &$output = "", $depth = 1) { $output .= "array(\n"; foreach ($attributes as $attribute => $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); } 




Kind KeepYourMind suggested in the comments that you can use the php function var_export () to generate, 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 the existing attributes.

 <?php $form = $this->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 signatures of the attributes, we will have to strictly 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 have not found another normal way to do this. You can put them into an additional file, but this 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 "<?php\nreturn " . $output . ";\n"; } public function generateConfigFileRecursive($attributes, &$output = "", $depth = 1) { $output .= "array(\n"; foreach ($attributes as $attribute => $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 typographical errors, please report through private messages. I apologize for typos in advance - for a long time I didn’t write anything except code in such volumes

Source: https://habr.com/ru/post/173153/



All Articles