В любом веб проекте используются конфигурационные файлы. Чаще всего они редактируются на стадии разработки или переносе проекта а потом надолго остаются нетронутыми. Но бывает что конфиг разрастается и в нем появляются параметры которые приходится изменять чаще.
Задача довольно тривиальная, создать форму которая позволяет изменить значения некоторых параметров и занести изменения в существующий файл. Но когда часто сталкиваешься с однотипной задачей хочется найти какое-то более менее универсальное решение чтобы не писать каждый раз унылый велосипед.

К сожалению гугл не дал простого и функционального решения, не знаю может гугл на меня за что то зол:). Возможно есть какое-то решение но подходящей мне я не нашел. Прошу сильно не ругаться, если таковой всё-таки существует. Задумался, а какой бы функционал меня бы устроил.

Во первых это вывод на редактирование только тех параметров которые нужны, во вторых это чтобы некоторые параметры выводились как checkbox-ы, radio кнопки если это соответствует их логике ну и не мешало-бы чтобы параметры имели красивое название при выводе.

Если наглядно то чтобы из этого:


Можно было получить это:


При этом чтобы все было просто и надежно.

И так приступим. Конфигурационные файлы часто имеют вид файла php с определениями разных констант переменных и прочего. Такой формат сложно както уверено редактировать, гораздо проще и не менее распространённый формат, когда весь конфигурационный файл представляет из себя массив данных. Вот с этим можно что-то придумать. Работать будем с максимум двумерными массивами, для большей глубины не думаю, что решение будет уже универсальным. К тому же конфиг может содержать такие сложные данные, однако их редактирование будет невозможно.

Данные второго уровня выводятся в отдельном блоке fieldset который позволяет визуально разграничивать разные наборы параметров, также это блок можно сворачивать кликом мыши.

Для того чтобы не изменяя данные массива иметь возможность его как-то описать будем использовать обычные php комментарии. Комментарии которые будут на одной строке с значением будут описывать его и создавать определенное поведение при созданий формы. Во первых это вывод более содержательного названия для полей. Ищем регулярным выражением комментарии на той же строке что и определение поля.

// Parse atributes of values from config file 
    public function getAtributes() {
        $conf_str = file_get_contents($this->file_name);
        foreach ($this->data as $n => $cf) {
            // First level of array 
            if (preg_match("#" . $n . "[^\r\n]*//([^\n\r]+)[\n\r]#", $conf_str, $m)) {
                $this->atribute[$n] = trim($m[1]);
            }
            if (is_array($cf)) {
                //second level 
                foreach ($cf as $ns => $vs) {
                    if (preg_match("#" . $n . ".*" . $ns . "[^\r\n]*=>[^\r\n]*//([^\n\r]+)[\n\r]#isU", $conf_str, $m)) {
                        $this->atribute[$n . "~" . $ns] = trim($m[1]);
                    }
                }
            }
        }
    }

сохраняем все полученные данные и будем выводить в форму только те что имеют описание.
Дальше не плохо было бы в этом описании иметь возможность вводить какие-то ключи, которые будут влиять на тип отображаемого элемента формы. Например задавать допустимые для текущего параметра значения, формат был выбран такой [options| знач1|…|значn].

Парсим полученные комментарии, опять же регуляркой чтобы выудить из них список значений. Решил не добавлять возможность указывать какой именно тип инпута должен быть выведен, все это логично исходит из предложенных значений. Если значений только 2 и они обе булевского типа то можно выводить checkbox, если значений больше, то выводим радиокнопки а если уж совсем много то рисуем select. Для полей которые не имеют каких-то опции выводим универсальный текстовый input.

    // Parsing options from values atribute 
    public function parseAtribute($atribute) {
        $flags = array();

        $flags['title'] = $atribute;
        //can't delete this value
        if (strpos($atribute, "[static]") !== false) {
            $flags['static'] = true;
        } else {
            $flags['static'] = false;
        }
        //can add values in this sub array
        if (strpos($atribute, "[dinamic]") !== false) {
            $flags['dinamic'] = true;
        }
        //toggled block by default
        if (strpos($atribute, "[hidden]") !== false) {
            $flags['hidden'] = true;
        } else {
            $flags['hidden'] = false;
        }
        //parsing options that can by values for curent input
        //select type of input 
        if (strpos($atribute, "[options") !== false) {
            preg_match("#\[options\|(.+)+\]#", $atribute, $options);

            $options = explode("|", $options[1]);
            if (sizeof($options) == 2) {
                // if have 2 options and both its from boolean type 
                $checkbox = true;
                foreach ($options as $od) {
                    if (!in_array($od, $this->booleanValues(), true)) {
                        $checkbox = false;
                    }
                }
                if ($checkbox) {
                    $flags['options']['type'] = "checkbox";
                } else {
                    $flags['options']['type'] = "radio";
                }
            } elseif (sizeof($options) < $this->optForSelect) {
                $flags['options']['type'] = "radio";
            } else {
                $flags['options']['type'] = "select";
            }
            $flags['options']['data'] = $options;
            // parse labels for values and clear data
            foreach ($options as $n => $v) {
                if (preg_match("#(.*)\((.*)\)#", $v, $m)) {
                    $flags['options']['data'][$n] = trim($m[1]);
                    $flags['options']['labels'][$n] = $m[2];
                }
            }
        } else {
            $flags['options'] = false;
        }
        // clear title of input 
        $flags['title'] = trim(preg_replace("#\[[^\[\]]*\]#", "", $flags['title']));

        return $flags;
    }

В вариантах значений можно указывать красивые названия в обычных скобках чтобы при созданий списков или кнопок выводить их а не безжизненные, сухие цифры типа 0,1,2. К сожалению такой формат не дает возможности делать варианты значений содержащие символы скобок "(" и ")".

Пришлось много повозиться с обработкой checkbox- ов, от них передается значение либо «on» либо вообще пустое. Приходится сравнивать со списком собранных атрибутов и обрабатывать пустое значение как булевский ноль. Свойства класса trueValues и falseValues содержат значения, которые могут быть интерпретированы в двоичном смысле 0-1, true-false, yes-no.

	//Convert values to 0 or 1 
	public function toBool( $val ){
		if( in_array($val, $this->trueValues) )
			return 1;
		if( in_array($val, $this->falseValues) )
			return 0;
		
		return false;
	}

Для того чтобы случайно не затереть данные или чтобы исключить влияние какихто багов при первом запуске класс создает копию конфига. И в любой момент можно вернуться к изначальной версии.

Ну а когда все данные из конфига выужены можно рисовать форму. Пришлось ввести в класс и вывод html верстки но зато все решение состоит из одного файла который полностью решает постовленую задачу. Jquery используемый для некоторых манипуляций элементами формы, подключается на лету если не был подключен до этого. Например filedset можно свернуть что позволяет легче ориентироваться на форме большого размера и найти нужный параметр.
Для удобства некоторые блоки параметров можно указать изначально свернутыми опцией [hidden].

Для данных второго уровня можно добавить возможность создавать дополнительные поля, имя поля будет сгенерировано автоматически, а вот значение можно ввести свое. Если для каких-то задач это потребуется, то добавьте ключ [dinamic] в определении родительского поля. Если все же некоторые поля внутри этого должны оставаться нетронутыми, то укажите для них ключ [static].

Возможно, кому-то пригодится данный класс. Старался сделать максимально простой для использования и достаточный чтобы быть полезным функционал. В конце концов для работы нужно просто написать:

<?php
include "../configer.php";
$cf = new Configer("settings.php");
$cf->showForm();

Ну и чтобы в вашем файле settings были какието комментарий с ключиками.

Скачать класс можно на https://github.com/vencendor/Configer.

Буду рад полезным замечаниям.

UPD: Рекомендации учтены, к сожалению на момент появления статьи на github.com не была актуальная версия класса.
Поделиться с друзьями
-->

Комментарии (22)


  1. SerafimArts
    16.11.2016 12:38
    +3

    PSR? «array()»? открывающий «<?»? Composer? И в качестве добивающего «var» в объявлении полей класса.

    Шёл 2016ый год…


    1. SerafimArts
      16.11.2016 12:47
      +2

      P.S. Я уж не говорю про смешение логики и представление — это само собой очевидно и говорит лишь о небольшом опыте.

      Но использование var… Это же PHP начала 2000х годов, когда в мире царствовала версия 4. Это даже если учитывать небольшой опыт — надо очень сильно постараться, чтобы найти какой-то «учебник», где такое написано, примерно как ПК с Pentium 3 на борту. Просто раритет и подрастающее поколение такое даже в глаза не видело нынче.


      1. vlreshet
        16.11.2016 13:03
        +1

        Да тут весь код такой что при чтении хочется забиться в уголок обнять колени и рыдать


  1. MetaDone
    16.11.2016 13:06

    http://symfony.com/doc/current/components/yaml.html#writing-yaml-files
    гибко, читаемо, не костыльно.


  1. ellrion
    16.11.2016 13:11
    +1

    Идея может и неплохая, но код… Приведите код к psr-2. Смените кодировку на utf-8. Добавьте тесты. Замените var на нормальную область видимости. Вынесите шаблон и JS код отдельно от класса.


    1. Vencendor
      16.11.2016 13:16
      -3

      Спасибо учту. Пока нет необходимости разделять на файлы. Суть в том чтобы инструмент был максимально компактным и состоял из одного файла.


      1. ellrion
        16.11.2016 13:38
        +1

        максимально компактный но зависимость от jquery? И как вы сами вообще умудряетесь там всё править в этой мешанине. Ну уже хотя бы как то разделили код метода showFormна части по внутренним темплейтам в heredoc. или вообще зачем там класс тогда, сделали бы просто скрипт, тоже было бы в одном файле но чище. Ну или phar есть.


        На самом деле я удивляюсь вашей "смелости". Такое на хабр выкладывать… Идея спорная всё же. А исполнение хуже некуда.


      1. vlreshet
        16.11.2016 14:14

        Уже не то время когда компактность ценится. Разработчику гораздо проще выполнить одну команду в композере, чем качать ваш файлик, потом думать куда его приткнуть, потом подключить его ещё где-то… И как заметил комментатор выше — jquery тут излишний.


    1. ellrion
      16.11.2016 13:19
      +1

      А еще у вас всё плохо с именами переменных. Date где по логике data, safe где должно быть save, Attrebut с одной t. Это я уже не говорю о семантической корректности например имени метода getAtributes


      1. Vencendor
        16.11.2016 13:47

        отформатировал. переименовал переменные


  1. vlreshet
    16.11.2016 13:15

    Зачём вообще нужен визуальной редактор для такой вещи как конфиг? Это же не та вещь которая изменяется каждый день, и без удобств ну совсем никак. Плюс это потенциальная уязвимость.


  1. Ualde
    16.11.2016 13:15

    Самое удивительное, что часто конфигурация имеет больше 2-х уровней вложенности, и тут все перестает работать…


    1. Lure_of_Chaos
      16.11.2016 13:52

      Плюс, сильно зависит от форматирования и правильности комментариев в конфиге


  1. oxidmod
    16.11.2016 13:37

    ИМХО, не стоит давать мышкой тыкать в конфигах, а то натыкают))


    1. Vencendor
      16.11.2016 13:53

      в основных конфигах точно. Но вот всякие параметры типа количество объектов на странице, емайл, и прочее можно вывести для сохранности в отдельный файлик.
      Ну и на худой конец класс сразу же создает бэкап который позволяет вернуться к первоначальной версии.


      1. Lure_of_Chaos
        16.11.2016 14:01

        Я бы вообще для визуального редактирования, если уж очень понадобилось, использовал бы какой-нибудь свой формат, с экспортом и опционально импортом, а конфиги напрямую редактировать не давал бы — только ручками (по фтп, например)


  1. zenn
    16.11.2016 13:52
    +2

    Извините, но что это за убожество и как оно попало на habr? Автор, вы в php 2 недели отроду? Я пожалуй впервые вижу столь извращенный велосипед там, где он совсем не нужен и там, где есть отлично документированные нативные функции php. Отбросим следование стандартам, psr, best practice и другие концепции, давайте по факту.
    1. Логика чтения и записи в файл конфигов. Я или очень сильно отстал от жизни, или чего-то не знаю, но зачем делать вот это:

    public function getAtributes() {
    		$conf_str=file_get_contents($this->file_name);
    		foreach($this->date as $n=>$cf) {
    			if(preg_match("#".$n."[^\r\n]*//([^\n\r]+)[\n\r]#",$conf_str,$m))
    				$this->atribute[$n]=trim($m[1]);
    		}
    	}
    

    Что вам мешает иметь конфиги вида:
    <?php
    return [
        'param1' => [
            'param2' => 'value'
        ]
    ];
    

    и делать прямой инклюд данного конфиг-файла как то так:
    .... 
    if (!file_exists($path)) {
        return;
    }
    $configArray = include($path); // look at ex#5 http://php.net/manual/en/function.include.php
    if (!is_array($configArray)) {
        return;
    }
    

    и вы получите тот же самый массив конфига без извращений через preg_match*, притом что ниже в конструкторе вы так делаете сами (вопрос — а на*рена этот конструктор там вообще?)
    public function __construct($file_name) {
        // ....
        $this->date = require $this->file_name;
        // ..... 
    }
    

    Поехали дальше. Зачем вы делаете замену значений по ключу в строке?
    public function safe(){
        $conf_str=var_export($this->date,true);
        //$atr=$model->getAtribute();
        foreach($this->atribute as $n=>$v) {
            $conf_str=preg_replace("#([^\n\r]*".$n."[^\n\r]*)[\n\r]#is","\\1//".$v."\n",$conf_str);
        }
        file_put_contents($this->file_name,"<? \n return ".$conf_str." \n ?>");
    }
    

    выглядит так, будто вы специально пытаетесь выстрелить себе в ногу или сломать ее о свой кривой велосипед. Почему вы не работаете со своим же массивом $this->data не обновляя в нем значения переменных из входящего POST а позже не конвертируете финальный результат в str (var_export)? Какую цель вы преследуете, заменяя значения через preg_replace в сконвертированной строке?
    2. Смешались в кучу кони-люди. Зачем вы используете синтаксис html/css в теле функций на echo? Я видал всякое, видал echo внутри php конструкций, видал echo и return'ы внутри функций, но чтобы в теле класса содержимое метода представляли вот так:
    <?php
    class Configer {
        	function showForm(){ ?>
    		
    <style>
    	#configForm span{display:inline-block; width:150px; }
    }
    

    я вижу впервые… не нужно так.


    1. ellrion
      16.11.2016 14:08
      +1

      танцы с preg_match и preg_replace там я так понял из-за комментариев. Другое дело что благодаря коду это не очевидно)


      1. zenn
        16.11.2016 14:21

        Да, я это уже понял позже когда более пристально глянул в «код», если его так можно назвать. Стоит ли оно того конечно вопрос (выходит что конфиг инклюдится, а позже считывается, а при сохранении замены ведутся перебором цикла, и тут шаг в сторону будет равен выстрелу в ногу).


    1. Lure_of_Chaos
      16.11.2016 14:13

      Ну вообще код, представленный в топике, попадает в раздел «не нужно, но было бы приятно прихерачить сюда гуй — гуяк гуяк и в продакшн через 2 часа», и никогда не должен выкладываться на хабр.
      Ибо сюда заходят люди, которые хотят прочитать что-то действительно новое и интересное.
      А подобные топики нещадно заминусовываются вместе с кармой автора.

      У самого таких поделок вагон и маленькая тележка, но они редко попадают даже на личный гитхаб, ибо смысл?


      1. Rottenwood
        16.11.2016 16:32
        +1

        Возможно смысл в том, чтобы получить критику от сообщества. Например, zenn потратил время на анализ представленного автором, а значит конвертация кармы в опыт вполне может быть оправданна.


        1. SerafimArts
          16.11.2016 19:12
          +2

          У автора под рукой интернет и если бы он потрудился изучить статьи других авторов — не раз бы наткнулся на ссылку: http://getjump.me/ru-php-the-right-way/ и мог хотя бы попытаться следовать, пусть не всем, но хотя бы большинству рекомендаций. Моё мнение, если бы автор стремился к критике сообщества — он бы мог её получить и без хабрасуицидов.

          P.S. В нашем же случае — автор просто положил большой болт и понадеялся на непонятно что. Да, можно оценивать его работу, как действительно какую-то работу и сказать ему «спасибо» за это. Ну вклад в сообщество и всё такое…

          Но тут немного иная ситуация: Подобная задача решается намного проще, быстрее и качественнее, так что такой «вклад» не только не нужен, сколь вреден. И вреден не только как пример «как не надо писать», но и тем, что он может навредить тому, кто решится его использовать, начиная с безопасности, заканчивая повреждением исходных файлов конфигураций.