Загружаем сниппеты и чанки с диска


Кроме нескольких осведомленных человек, есть люди, которые подозревают, что в MODX все элементы хранятся в БД. У сообщества есть твердое мнение, что в данном решении больше минусов, чем плюсов — и время затрачивается на запросы к базе, и разрабатывать не очень удобно (хоть и появились недавно статические элементы), но главное — это невозможность нормального деплоя и поддержки версионности. Мне эта мысль тоже не давала покоя. Поэтому я вчера вечерком решил поэкспериментировать с любимым pdoTools. Дописал в парсер несколько строчек и добавил в pdotools.class.php функцию runSnippet.

Теперь парсер умеет загружать чанки и сниппеты с диска. Для этих элементов я ввел специальный токен — знак подчеркивания (все остальные уже заняты). И если парсер встречает такие тэги
[[!_snippet]] 
// или
[[!$_chunk]]
он не лезет в базу, а грузит их с диска.
А как он понимает, откуда грузить? По-умолчанию, он ищет их в /core/elements/snippets и /core/elements/chunks соответственно для сниппетов и чанков. Эти пути можно переопределить в системных настройках — snippet_files_path и chunks_files_path.

Понятно, что хранить все чанки и сниппеты в одной папке не удобно. Поэтому в имени чанка или сниппета можно указывать папки через точку, как мы это делаем при загрузке классов. Т.е. при обработке такого тэга
[[!_category.mysnippet]]
парсер будет искать сниппет с именем mysnippet.php в папке /core/elements/snippets/category/, а при обработке тэга
[[!$_category.mychunk]]
парсер будет инклюдить чанк /core/elements/chunks/category/mychunk.tpl.
Из вышеописанного видно правило — у сниппетов расширение должны быть «php», а у чанков — «tpl».

ОТЛИЧИЯ РАБОТЫ СО СНИППЕТАМИ
Тут нужны пояснения. Правила вызова этих чанков и сниппетов в ресурсах не изменились. Вызываем как обычно с любыми фильтрами и тэгами. Но немного изменилась логика работы со сниппетами.
Во-первых, изменился порядок определения переменных из $scriptProperties. Раньше параметры, указанные при вызове сниппета, переопределяли значения по умолчанию. Теперь значений по умолчанию нет. Их нужно самим определять в сниппете. А параметры вызова передаются в сниппет. Это важно учитывать. Поясню. При вызове сниппета
[[!_snippet? &param1=`1` &param2=`2`]]
внутри сниппета уже будут определены $scriptProperties['param1'] и $scriptProperties['param2'], а также $param1 и $param2. Это нужно учесть, чтобы случайно не затереть их.
Чтобы этого избежать, в сниппете нужно проверять их наличие
$scriptProperties['prop'] = isset($scriptProperties['prop']) ? $scriptProperties['prop'] : 'Новое значение';
// или так
$scriptProperties['prop'] = isset($prop) ? $prop : 'Новое значение';
// Или так
/* параметры по умолчанию */
$properties['prop1'] = 'Новое значение 1';
$properties['prop2'] = 'Новое значение 2';
/* Сливаем параметры */
$scriptProperties = array_merge($properties,$scriptProperties);
Во-вторых, возвращать значение из сниппета нужно не через return, а как обычно через print или echo. Мне кажется это привычнее для php разработчиков.
И в третьих, в данном случае нужно переделывать вызовы сниппетов с набором параметров. Так как не имеет смысла лезть за ними в базу коли мы уж совсем отказываемся от такого принципа. Т.е. их также нужно определять в самом сниппете. Такой код работать не будет
[[!_mysnippet@propertiesSet]]

ФУНКЦИЯ pdoTools::runSnippet()
Некоторые знают, что в pdotools есть функция getChunk(). Если вызвать ее так
$pdoTools->getChunk('@FILE /chunks/mychunk.tpl');
то чанк загружается с диска.
Я для комплекта добавил функцию runSnippet('Имя сниппета', параметры, путь), который делает тоже самое, т.е. грузит сниппет с диска. Вызывается он так
$path = $modx->getOption('snippet_files_path',null,$modx->getOption('core_path').'elements/snippets/');
$res = $pdoFetch->runSnippet('@FILE mysnippet.php',array('prop1'=>1),$path);
// Можно и без последнего параметра. По-умолчанию, $path = '/core/elements/snippets/'.
// А в имени сниппета можно указать подпапку
$res = $pdoFetch->runSnippet('@FILE category/mysnippet.php',array('prop1'=>1));
Если не указан @FILE, то вызовется обычный сниппет MODX.

Таким образом, мы получаем вполне пригодный механизм для улучшения поддержки версионности и деплоя, а также более удобную разработку. По выигрышу по времени не готов дать точную выкладку. Полноценный тест не проводил. Но думаю, что выигрыш должен быть.
В общем жду экспертную оценку моего творчества. Какие подводные камни могут быть? Нужно ли включать такой функционал в pdoTools? Это вопрос, конечно, к Василию.

П.С. Также не проверял работу с феномом. Еще с ним не разобрался. Жду курса Василия, чтобы ускорить процесс ознакомления. По идее все должно работать.

ИТОГ
Данное решение признано не перспективным.
04 октября 2015, 08:58    Сергей Шлоков   G+  
1    2095 +5

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

  1. Павел Гвоздь 04 октября 2015, 11:14 # +1
    Но немного изменилась логика работы со сниппетами
    А нельзя всё привести к общему стандарту MODX? Ведь это лишняя проблема помнить о том, что там так, а тут не так. Придётся переписывать сниппеты, которые работали из базы, и не работают в pdoTools. Если честно, мне это решение кажется каким-то костыльным, чтоли. Извини, если покажусь грубым, не планировал этого.

    По поводу print vs return в корне не согласен.

    А вообще плюсанул за труд.
    1. Сергей Шлоков 04 октября 2015, 11:49 # 0
      А нельзя всё привести к общему стандарту MODX? Ведь это лишняя проблема помнить о том, что там так, а тут не так.
      Это проблема для людей, которые учили php через MODX. Как и я собственно, начал учить php разбираясь со снипетами. Но у php свои правила и стандарты.
      В MODX перед вызовом сниппета зачитываются его параметры по умолчанию, к ним добавляются параметры вызова и все это передается в сам сниппет. А в данном случае, если сниппета в базе нет, то нет и параметров по умолчанию (хранящихся в поле properties таблицы modx_site_snippets). В данном случае их негде взять. Поэтому, их нужно определять самостоятельно. Но я не вижу в этом проблемы. Многие, в том числе и я, в своих сниппетах проверяют эти параметры. Вот пример. При таком подходе вообще переделывать ничего не нужно.
      Тоже и по поводу return. Для php программиста привычнее и логичнее print. А для MODX-программиста — return. На самом деле ничего сложного поменять логику нет. Пара строчек кода. Вопрос именно в подходе — делать как в обычном php или как в MODX.
      Если честно, мне это решение кажется каким-то костыльным, чтоли.
      Мнения разные важны. В данном случае не соглашусь. Это совсем не костыли. Тут работа именно с чистым php по его же стандартам. Т.е. человеку, который знает php и решил разрабатывать на MODX не нужно знать тонкости и условности этого движка. Как привык так и пиши. Очень часто именно это является камнем в огород со стороны пользователей других движков.
      1. Николай Ланец 04 октября 2015, 12:38 # +2
        По поводу print vs return в корне не согласен.
        +1

        Тоже и по поводу return. Для php программиста привычнее и логичнее print. А для MODX-программиста — return. На самом деле ничего сложного поменять логику нет. Пара строчек кода. Вопрос именно в подходе — делать как в обычном php или как в MODX.
        Сергей, вы здесь в корне не правы, сорри. Поясню. Просто проследите ход выполнения метода MODx::runSnippet(). Самое важное: $output= $snippet->process($params); Уточняю: в данном случае выполняется присвоение. Вывод print/echo просто так присвоить нельзя. Для этого в modScript (расширяемый классом modSnippet) используется костыль ob_start()/ob_get_contents()/ob_end_clean(), и используется он как раз потому что многие именно MODX-разработчики вместо return пишут print/echo в своих сниппетах. Таким образом им просто облегчили жизнь. Но все-таки правильно именно возврат значения делать через return;
        Еще аргумент: есть негласное правило у программистов: любая функция должна выполнять return, даже если она ничего не возвращает. Это как минимум для читабельности кода. Так вот, сейчас в modScript выполняется include кеш-файла сниппета, а вот еще каких-то пару лет назад код сниппета преобразовывался в функции и сниппет вызывался именно как функция. А еще раз повторюсь: функции должны выполнять возврат, а не принт.
        1. Сергей Шлоков 04 октября 2015, 12:57 # 0
          Я не большой профи в php. Просто мне кажется, что раз php файлы лежат отдельно, они не должны включать логику MODX. А пользователи yui, laravel тоже в инклюдах прописывают return?
          Я не собираюсь отстаивать именно print. Тут как раз я хочу получить экспертное мнение как лучше.
          Самое важное: $output= $snippet->process($params);
          Это как раз и не самое важное. Присвоение идет внутри process(). Отличие лишь в том, что для return надо прописывать
          $includeResult= include $this->_scriptFilename;
          а для print нет
          include $this->_scriptFilename;
          Просто мне казалось, что для стороннего php программиста сниппет будет выглядеть привычнее с print. Он может написать любой сниппет для MODX менеджера, который сможет его подключить. Я так думаю. Я могу и ошибаться, ибо не профи в этом.

          1. Николай Ланец 04 октября 2015, 13:26 # +2
            А пользователи yui, laravel тоже в инклюдах прописывают return?
            Я не специалист по ларке, но даже при быстром осмотре доки встречается такое:
            Откройте файл app/routes.php и добавьте этот код в его конец:
            Route::get('users', function () {
            return 'Users!';
            });
            Теперь если вы перейдёте в браузере на адрес .../users, то должны увидеть текст Users!
            Самое важное: $output= $snippet->process($params);
            Это как раз и не самое важное. Присвоение идет внутри process().
            Вы не поняли. Без присвоения и возврата во внутренних вызовах, не будет возврата и во внешних. Вот смотрите, здесь выполняется присвоение и возврат, и здесь выполняется присвоение и возврат.
            А то, как у вас сейчас сделано, просто не позволит мне выполнить что-то типа такого:
            $result = include "path_to_snippet";
            $result = some_method($result);
            return $result;
            Мне в таком случае опять-таки придется использовать костыль с обфлэшем.

            Отличие лишь в том, что для return надо прописывать
            $includeResult= include $this->_scriptFilename;
            а для print нет
            include $this->_scriptFilename;
            Ничто вам не мешает писать print include $this->_scriptFilename;

            Просто мне казалось, что для стороннего php программиста сниппет будет выглядеть привычнее с print. Он может написать любой сниппет для MODX менеджера, который сможет его подключить. Я так думаю. Я могу и ошибаться, ибо не профи в этом.
            Куча принтов в сниппетах — это всегда было плохо. Пусть мне кучу минусов к коменту напихают несогласные. Сниппет — это логика. Она не должна ничего принтить. Принт — это вопрос шаблонизации, а это уже дело шаблонов и чанков. Но чанки опять-таки должны вызываться кодом-обработчиком.
            1. Сергей Шлоков 04 октября 2015, 13:36 # 0
              Вы не поняли. Без присвоения и возврата во внутренних вызовах, не будет возврата и во внешних. 
              У меня есть и присвоение и возврат. Вот код для принта
              include $this->_scriptFilename;
              if (ob_get_length()) {
              	$this->_output = ob_get_contents();
              }
              
              Я не собираюсь упорствовать. Должен быть, пусть будет. Тем более, что это все равно оказалось никому не нужно :)
              1. Николай Ланец 04 октября 2015, 13:40 # 0
                Ну, это и есть костыль)
          2. Сергей Шлоков 04 октября 2015, 13:10 # 0
            Для этого в modScript (расширяемый классом modSnippet) используется костыль ob_start()/ob_get_contents()/ob_end_clean(), и используется он как раз потому что многие именно MODX-разработчики вместо return пишут print/echo в своих сниппетах. Таким образом им просто облегчили жизнь.
            Во-первых, не знал, что это костыль.
            Во-вторых, они (MODX-разработчики) потому и пишут, что привыкли на php так делать. А тут надо знать про return, именно знать, ибо об это нигде не написано.
            1. Николай Ланец 04 октября 2015, 13:28 # -1
              Дайте ссылку, где они такое пишут. Не видел ни одного сниппета от MODX-девов, где выполнялся бы принт. Возьмите тот же Wayfinder. Выполняется return;
              1. Сергей Шлоков 04 октября 2015, 13:39 # 0
                Это MODX-девы (как вы выразились), для которых и сделан вывод через буфер. Не разработчики-авторы MODX, а те программеры, которые решили освоить MODX.
                Ретурн так ретурн. В принципе, какая разница. Завтра забудем. :)
                1. Николай Ланец 04 октября 2015, 13:42 # 0
                  Ясно. Мы просто видимо с разными девами общаемся)
          3. Павел Гвоздь 04 октября 2015, 14:12 # 0
            Я PHP не по MODX учил. =)
        2. Василий Наумкин 04 октября 2015, 12:18 # +3
          По выигрышу по времени не готов дать точную выкладку. Полноценный тест не проводил. Но думаю, что выигрыш должен быть.
          С этого нужно было начинать. Без серьёзного улучшения производительности говорить не о чем.

          На мой взгляд вряд ли сниппеты станут работать быстрее, ибо они и сейчас все кэшируются в /core/cache/includes/elements/, а потом подключаются как файлы.

          В общем жду экспертную оценку моего творчества. Какие подводные камни могут быть? Нужно ли включать такой функционал в pdoTools?
          Я считаю, что нет, не нужно.

          Со времен Evolution в любом сниппете можно сделать
          include 'file.php';
          и работать с файлами. Есть статические элементы, есть Gitify, есть установочные пакеты (лично я переношу изменения через них).

          Короче, без серьёзного профита в удобстве или скорости это просто любопытный эксперимент, не более.
          1. Сергей Шлоков 04 октября 2015, 12:48 # +1
            Со времен Evolution в любом сниппете можно сделать
            include 'file.php';
            В сниппете да, а в ресурсе, чанке, шаблоне? И без фильтров и других примочек MODX.
            Есть статические элементы, есть Gitify, есть установочные пакеты
            Статические элементы, как мы знаем от Евгения Борисова, на продакшене нужно отключать.
            Gitify для меня как высшая математика. Для простых пользователей очень сложно. Установочные пакеты — мейби.
            Короче, без серьёзного профита в удобстве или скорости это просто любопытный эксперимент, не более.
            Мне кажется так удобнее и практичнее для разработки. Раз, создал файл. Два, в ресурсе его просто вызываешь как обычно. Все как и было, только файл не в базе, а на диске. Отредактировал, нажал F5 и все сразу обновилось. Используя статические элементы, например, нужно сначала кэш почистить.
            По скорости, допустим, сниппеты кэшируются, а чанки нет. За ними нужно лезть в базу.
            С этого нужно было начинать. Без серьёзного улучшения производительности говорить не о чем.
            Согласен, но у меня нет больших и сложных компонентов. А мне кажется, на маленьких данных преимуществ не будет видно. Но попробую ради интереса.
            П.С. Даже просто ради эксперимента, мне понравилось.
            1. Василий Наумкин 04 октября 2015, 12:53 # 0
              Чанки уже давным-давно можно вызывать из файлов через @FILE, так что о них смысла спорить нет.

              А какой профит будет от вызова сниппетов? Их что, 50 штук на странице вызывается, чтобы только от способа вызова оно тормозило? Сомневаюсь. Евгений свои исследования давно делал, с тех пор много версий MODX сменилось — нужно перепроверять.

              И если очень хочется сниппеты из файлов, в любом месте, смотри как это просто:
              <?php
              
              return include $path_to_file;
              Сниппет loadSnippet, который грузит любой код из файлов. Кажется, я это даже где-то уже видел на продакшене.

              В общем, еще раз, нужны тесты производительности.
              1. Сергей Шлоков 04 октября 2015, 13:05 # 0
                Чанки уже давным-давно можно вызывать из файлов через @FILE, так что о них смысла спорить нет.
                Не видел такой возможности в парсере, а как надо вызывать?

                <?php
                return include $path_to_file;
                Прикольно. Хорошее решение.
                1. Василий Наумкин 04 октября 2015, 13:31 # 0
                  [[!любойсниппетсpdotools?
                  	&tpl=`@FILE имяфайла.tpl`
                  	&tplPath=`путь\к\директории\с\шаблонами`
                  ]]

                  Если нужно без pdoTools, то просто еще один сниппет loadChunk:
                  <?php
                  return file_get_contents($path_to_file);

                  Пока я вижу только усложнение работы без явной выгоды. Указывать пути, отказываться от наборов параметеров, echo вместо return — зачем это всё?

                  Проблему деплоя это не решает, потому что вызовы сниппетов и чанков кто-то еще должен прописать в шаблонах или ресурсах — а их как делать статическими? Всё равно нужен какой-то скрипт распаковки, или установочный пакет.

                  А если у нас должен быть установочный пакет, то он и так всё может собрать — и сниппеты и чанки, и наборы параметров, и шаблоны, и ресурсы. Собственно, Theme.Bootstrap является простейшим примером такой установки.

                  Лично я так же пакетами разворачиваю изменения на modstore.pro и на modhost.pro — проблем пока не было.
                  1. Сергей Шлоков 04 октября 2015, 14:05 # 0
                    Проблему деплоя это не решает, потому что вызовы сниппетов и чанков кто-то еще должен прописать в шаблонах или ресурсах — а их как делать статическими?
                    Я и не писал, что это решает проблему деплоя. Я писал
                    Таким образом, мы получаем вполне пригодный механизм для улучшения поддержки версионности и деплоя, а также более удобную разработку.
                    Согласен, с шаблонами и плагинами тоже надо что-то придумывать. Особенно, с плагинами. Разработчики, как правило, в ресурсы не лезут.

                    Пока я вижу только усложнение работы без явной выгоды. Указывать пути, отказываться от наборов параметеров, echo вместо return — зачем это всё?
                    Никакого усложнения нет. На самом деле и наборы можно прикрутить. Вместо echo сделать return дело 2 секунд (я считал print лучше, меня переубедили). А уж про пути, конечно, прикольно. Т.е. вот это просто
                    [[!любойсниппетсpdotools?
                    	&tpl=`@FILE имяфайла.tpl`
                    	&tplPath=`путь\к\директории\с\шаблонами`
                    ]]
                    А у меня путь указать, который кстати, можно и не указывать, это невероятно сложно. Улыбнуло.
                    Про пакеты я писал, что это тоже решение. Просто кто как привык и какие задачи нужно решить.

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

                    Я это сделал не для поспорить. Мне было интересно поковыряться, экспертное мнение я услышал. Значит отложил в сторону. Пойду лучше с детьми погуляю, раз время свободное выдалось. :)

                    П.С. А за дефолтными параметрами сниппета MODX все равно в базу лезет. :)
                    1. Василий Наумкин 04 октября 2015, 14:42 # 0
                      А у меня путь указать, который кстати, можно и не указывать, это невероятно сложно. Улыбнуло.
                      И у меня можно путь не указывать, тоже есть директория по умолчанию. Но лично я этим сам не пользуюсь — или @INLINE чанки, или обычные. Файловые не прижились, почему-то.

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

                      Если только «чтобы было».
            2. Сергей Шлоков 05 октября 2015, 10:30 # +3
              Провел небольшой тест на своем сайте (на локалке). Ресурсов меньше 100. Тренировался только на чанках. Время вызова сниппета pdoMenu в чанке [[$main_menu]] отличается на 0.02 сек. Поэтому я его учитывать не стал.
              Вот шаблон страницы. Чанки простые, без фильтров и премудростей.
              <!DOCTYPE html>
              <html lang="ru">
              <head>
                  [[$head]]
              </head>    
              <body>
                  // Шапка страницы
                  [[$header]]
                  // Меню. В нем вызывается pdoMenu
                  [[$main_menu]]
                  // Галерея
                  [[$gallery]]
                  // Содержание ресурса 
                  [[*content]]
                  // Подвал страницы
                  [[$footer]]
                  </div> 
              </body>
              </html>
              
              Вызов в различных комбинациях. 5 раз для каждого метода. Показаны средние значения.
              --------------------------------------------------------------
              ВЫЗОВ КЭШИРУЕМЫХ ЧАНКОВ
              --------------------------------------------------------------
              // Чанки MODX из базы
                             Первый запуск            Второй (из кэша)
              Время:             0.74           |          0.21
              Запросы:            63            |          10
              // Чанки с диска
                             Первый запуск            Второй (из кэша)
              Время:             0.66           |          0.21
              Запросы:            58            |          10
              
              --------------------------------------------------------------
              ВЫЗОВ НЕКЭШИРУЕМЫХ ЧАНКОВ
              --------------------------------------------------------------
              // Чанки MODX из базы
                             Первый запуск                Второй 
              Время:             0.74           |          0.32
              Запросы:            63            |          12
              // Чанки с диска
                             Первый запуск                Второй
              Время:             0.66           |          0.27
              Запросы:            58            |          12
              
              --------------------------------------------------------------
              ВЫЗОВ НЕКЭШИРУЕМЫХ СТАТИЧЕСКИХ ЧАНКОВ
              --------------------------------------------------------------
              // Статические чанки MODX
                             Первый запуск                Второй 
              Время:             0.77           |          0.32
              Запросы:            65            |          12
              // Чанки с диска
                             Первый запуск                Второй 
              Время:             0.66           |          0.27
              Запросы:            58            |          12
              
              Видно, что запросов в базу стало меньше. Но выигрыш небольшой. По крайней мере, на небольших сайтах. При кэшировании разницы вообще нет. Может быть на больших сайтах с вызовом некэшируемых элементов эта разница будет заметнее. Но мне проверить негде.
              Вывод. На небольших сайтах результаты в районе погрешности. Преимуществ нет.
              Вот такое получилось исследование.

              П.С. Оставлю себе для разработки, чтобы не морочится со статическими элементами — каждому нужно прописывать пути в отличие от данного приема. Для простоты поменял логику парсера. Теперь он проверяет, есть ли такой чанк на диске, если есть — грузит, нет — берет из базы. Без всяких подчеркиваний.
            3. Илья Уткин 04 октября 2015, 13:14 # 0
              Эксперимент интересный. А вот, еще есть компонент SE Manager. Если его установить, можно спокойно работать с файлами. Сниппеты, шаблоны, чанки и даже плагины — всё в файлах.
              1. Сергей Шлоков 04 октября 2015, 14:06 # 0
                Ну да, я видел. Только это опять все через базу.
                1. Григорий Коленько 04 октября 2015, 17:02 # 0
                  Эм. Почему это через базу?
                  1. Сергей Шлоков 05 октября 2015, 09:36 # 0
                    Это у разработчиков MODX надо спрашивать.
                    1. Григорий Коленько 05 октября 2015, 11:57 # 0
                      Нет, я имел ввиду это инфа 100%? Я был уверен, что если указывается статичный файл для чанка, то он хранится только ФС
                      1. Сергей Шлоков 05 октября 2015, 16:51 # 0
                        Чанк грузится или из базы или из кэша. Когда он загрузится, идет проверка. Если чанк статичный, то сравнивается содержимое файла и чанка и если они не равны, то содержимое файла сохраняется в чанк в следующей строчке.

              2. Павел Гвоздь 04 октября 2015, 14:28 # 0
                Я считаю лучше освоить один раз работу с Gitify для решения проблем с версионностью, о которых писал автор поста. Тем более, что Иван Климчук поработал над переводом, за что ему огромное спасибо!
                1. Николай 04 октября 2015, 15:26 # 0
                  Если не нужны какие-то особенные функции для версионности, то есть отличный компонент — VersionX, который отслеживает изменения чанков, сниппетов, тв, шаблонов, ресурсов, плагинов. Всегда можно подсмотреть как выглядел сниппет, допустим, неделю назад, и восстановить его до этой версии.
                  1. Павел Гвоздь 04 октября 2015, 18:01 # 0
                    Про VersionX знаю. Но Gitify — это совсем другое. Первый ведь даже при каждом сохранении бекапит версию объекта, насколько я помню, и это никак не предотвратить?
                    1. Николай 05 октября 2015, 13:16 # 0
                      Вроде не при каждом, там кажется зависит от количества правок кода. Если немного, то не сделает бекапа.
                      1. Павел Гвоздь 05 октября 2015, 13:21 # 0
                        Странно, у меня даже если я сохраняю без изменений — создаётся бекап.
                    2. Николай 09 января 2017, 09:02 # 0
                      А сколько времени у versionx хранятся данные?
                  Вы должны авторизоваться, чтобы оставлять комментарии.