├── .gitignore ├── LICENSE ├── README.md ├── admin ├── applyError.php ├── cli.php ├── controller.php ├── createScenario.php ├── detail.php ├── log.php ├── main.php └── menu.php ├── composer.json ├── data └── scenarioTemplate.tpl ├── description.ru ├── docs ├── builders.md ├── cli.md ├── img │ ├── cli_action_help.png │ ├── cli_apply_all.png │ ├── cli_apply_hash.png │ ├── cli_apply_skip.png │ ├── cli_create.png │ ├── cli_create_full.png │ ├── cli_create_quick.png │ ├── cli_help.png │ ├── cli_history_count.png │ ├── cli_history_last.png │ ├── cli_list.jpg │ ├── cli_list.png │ ├── cli_rollback.jpg │ ├── cli_rollback_count.png │ ├── cli_rollback_hash.png │ ├── cli_rollback_last.png │ ├── cli_rollback_up_to.png │ ├── create_scenario.png │ ├── install_market.jpg │ ├── main.jpg │ ├── project_state.png │ ├── settings_page.jpg │ └── update_scenario.jpg ├── scripts.md ├── setup.md └── web.md ├── include.php ├── install ├── admin │ └── ws_reducemigrations.php ├── db │ ├── install.sql │ └── uninstall.sql ├── form.php ├── index.php ├── tools │ └── migrate ├── uninstall.php └── version.php ├── lang ├── en │ ├── admin.php │ ├── info.php │ ├── menu.php │ ├── setup.php │ ├── tests.php │ └── uninstall.php └── ru │ ├── admin.php │ ├── info.php │ ├── menu.php │ ├── setup.php │ ├── tests.php │ └── uninstall.php ├── lib ├── applyresult.php ├── builder │ ├── agentbuilder.php │ ├── builderexception.php │ ├── entity │ │ ├── agent.php │ │ ├── base.php │ │ ├── enumvariant.php │ │ ├── eventmessage.php │ │ ├── eventtype.php │ │ ├── fieldwrapper.php │ │ ├── form.php │ │ ├── formanswer.php │ │ ├── formfield.php │ │ ├── formstatus.php │ │ ├── highloadblock.php │ │ ├── iblock.php │ │ ├── iblocktype.php │ │ ├── property.php │ │ ├── table.php │ │ └── userfield.php │ ├── eventsbuilder.php │ ├── formbuilder.php │ ├── highloadblockbuilder.php │ ├── iblockbuilder.php │ ├── iblockpointer.php │ ├── tablebuilder.php │ └── traits │ │ ├── containuserfieldstrait.php │ │ └── operateuserfieldentitytrait.php ├── collection │ └── migrationcollection.php ├── console │ ├── command │ │ ├── applycommand.php │ │ ├── basecommand.php │ │ ├── createscenariocommand.php │ │ ├── helpcommand.php │ │ ├── history.php │ │ ├── listcommand.php │ │ └── rollbackcommand.php │ ├── console.php │ ├── consoleexception.php │ ├── formatter │ │ ├── output.php │ │ └── table.php │ ├── pear │ │ └── consoletable.php │ ├── runtimecounter.php │ └── runtimefixcounter.php ├── dumbmessageoutput.php ├── entities │ ├── appliedchangeslog.php │ ├── appliedchangeslogmodel.php │ ├── baseentity.php │ ├── eventmessage.php │ ├── setuplog.php │ └── setuplogmodel.php ├── exceptions │ ├── multipleequalhashexception.php │ └── nothingtoapplyexception.php ├── factories │ └── datetimefactory.php ├── localization.php ├── messageoutputinterface.php ├── migrationapplier.php ├── migrationrollback.php ├── module.php ├── moduleoptions.php ├── options.php ├── scenario │ ├── exceptions │ │ ├── applyscenarioexception.php │ │ └── skipscenarioexception.php │ └── scriptscenario.php ├── tests │ ├── abstractcase.php │ ├── cases │ │ ├── agentbuildercase.php │ │ ├── errorexception.php │ │ ├── eventsbuildercase.php │ │ ├── formbuildercase.php │ │ ├── highloadblockbuildercase.php │ │ ├── iblockbuildercase.php │ │ └── tablebuildercase.php │ ├── result.php │ └── starter.php ├── timeformatter.php └── timer.php ├── options.php ├── prolog.php ├── scripts └── install.php └── updater.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 WorkSolutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Модуль миграций 2.0 2 | =============== 3 | 4 | Модуль миграций для CMS "1С-Битрикс" – быстрые и стабильные обновления баз данных проекта. 5 | 6 | Известно, что с развитием проекта изменению подлежат не только исходный код и алгоритмы бизнес логики, но так же добавляются или удаляются новые сущности или их поля. Для упрощения изменений и последующих обновлений данных и предназначен данный модуль. 7 | 8 | ## Возможности 9 | 10 | * Составление сценариев миграций с помощью специальных ```билдеров```, специально предназначенных для этих нужд. Таким образом создание новых сущностей данных будет происходить не труднее чем через административный интерфейс; 11 | * Актуализация данных. Бавают случаи когда данные нужно "подправить" не меняя структуры - миграции подходят для этого как нельзя кстати. Сценарий будет запущен один раз и для всех площадок; 12 | * Работа через командную строку. Обновление можно выполнять как вместе с обновлением исходного кода, так и использовать специальные инструменты систем версионирования - запуска скриптов после обновления. 13 | 14 | ## Преимущества 15 | 16 | * Стабильность. Сценарии миграций данных составляются и отлаживаются командой. 17 | * Удобство. Модуль обладает широким спектром функционала для манипулирования миграциями. 18 | * Информативность. Удобный вывод списков миграций при работе через консоль. 19 | * Предсказуемость. Можно указывать примерное время выполнения миграций – это будет спобоствовать правильному принятию решения при обновлении. 20 | 21 | ## Как это работает? 22 | 23 | Простейшая схема работы команды над проектом выглядит следующим образом: 24 | 25 | ![Схема работы над проектом](docs/img/project_state.png) 26 | 27 | Есть локальные площадки програмистов и есть сервера которые доступны "извне" через Интернет. Каждая площадка имеет отдельную базу данных. Базы данных площадок отличаются только наполнением, но схемы (таблицы, поля, инфоблоки и т.д.) данных одинаковы. 28 | 29 | Процесс изменения схемы данных (либо манипуляции над данными) которые нужны для каждой площадки следующий: 30 | 31 | 1. Необходимо сделать изменения в схеме данных на проекте. К примеру добавить поле в одну из сущностей, которое будет использоваться новой функцией. 32 | 2. Разработчик создает миграцию добавления нового поля. Миграцией будет являться файл (класс php), определенного формата, в котором необходимо написать сценарии обновления (добавления поля) и отката (удаления поля) на случай неудачного применения обновления или отката версии проекта на предыдущую. 33 | 3. Запускает миграцию на локальной копии проекта, отлаживает применение и откат миграции. 34 | 4. Регистрирует миграцию в системе версионирования. Так исходный код для запуска миграции распространится по всем платформам. 35 | 5. Каждая площадка получившая исменения исходного кода запускает обновления миграций. 36 | 37 | Таким образом новое поле в базу данных добавляется для всех площадок получивших изменения одинаковым образом. 38 | 39 | ## Что дальше? 40 | 41 | * [Устанавливаем модуль](docs/setup.md) 42 | * [Создаем сценарии миграций](docs/scripts.md) 43 | * [Используем построители сущностей](docs/builders.md) 44 | * [Работаем с готовыми сценариями миграций через командную строку](docs/cli.md) 45 | * [Миграции в административном интерфейсе](docs/web.md) 46 | -------------------------------------------------------------------------------- /admin/applyError.php: -------------------------------------------------------------------------------- 1 | array('=id' => $id))); 7 | if (!$model) { 8 | require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_admin_js.php"); 9 | ShowError($localization->message('error.modelNotExists', array( 10 | ':id:' => $id 11 | ))); 12 | require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/epilog_admin_js.php"); 13 | } 14 | /** @var CMain $APPLICATION */ 15 | 16 | require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_admin_js.php"); 17 | ?> 18 | 19 | 20 | 23 | 26 | 27 |
21 | message('message')?> 22 | 24 | getName(), $model->getErrorMessage()))?> 25 |
28 | 43 | printLine("Migrations module for CMS Bitrix. Worksolutions company https://worksolutions.ru \n"); 24 | }; 25 | 26 | $getShowProgress = function () use ($console) { 27 | $counter = new \WS\ReduceMigrations\Console\RuntimeCounter(); 28 | return function ($data, $type) use ($console, $counter) { 29 | if ($type == 'setCount') { 30 | $counter->migrationCount = $data; 31 | $counter->start = microtime(true); 32 | } 33 | if ($type == 'start') { 34 | $counter->migrationNumber++; 35 | $console->printLine(sprintf( 36 | '%s (%s/%s)', 37 | $console->colorize($data['name'], Console::OUTPUT_PROGRESS), 38 | $counter->migrationNumber, $counter->migrationCount 39 | )); 40 | } 41 | if ($type == 'end') { 42 | /**@var \WS\ReduceMigrations\Entities\AppliedChangesLogModel $log */ 43 | $log = $data['log']; 44 | $time = round($data['time'], 2); 45 | $message = ''; 46 | if (!empty($data['error'])) { 47 | $message .= ' ' . $data['error']; 48 | } 49 | $overallTime = round(microtime(true) - $counter->start, 2); 50 | $message .= sprintf(' - %s (%s)', $console->formatTime($time), $console->formatTime($overallTime)); 51 | if ($log->isSkipped()) { 52 | $message = ' - skipped'; 53 | } 54 | $console->printLine($message, $log->isFailed() ? Console::OUTPUT_ERROR : Console::OUTPUT_SUCCESS); 55 | $console->printLine(""); 56 | } 57 | }; 58 | }; 59 | try { 60 | $console->printLine(''); 61 | $command = $console->getCommand(); 62 | $command->execute($getShowProgress()); 63 | $fCompanyLabel(); 64 | } catch (\WS\ReduceMigrations\Console\ConsoleException $e) { 65 | $console->printLine($e->getMessage()); 66 | $fCompanyLabel(); 67 | } 68 | -------------------------------------------------------------------------------- /admin/controller.php: -------------------------------------------------------------------------------- 1 | isAdmin()) { 7 | return ; 8 | } 9 | 10 | CModule::IncludeModule('ws.reducemigrations'); 11 | 12 | $request = $_REQUEST; 13 | $action = $request['q']; 14 | $fAction = function ($file) use ($action) { 15 | global 16 | $USER, $DB, $APPLICATION, $adminPage, $adminMenu, $adminChain; 17 | $localization = \WS\ReduceMigrations\Module::getInstance()->getLocalization('admin')->fork($action); 18 | include $file; 19 | }; 20 | 21 | $actionFile = __DIR__.DIRECTORY_SEPARATOR.$request['q'].'.php'; 22 | if (file_exists($actionFile)) { 23 | $fAction($actionFile); 24 | } else { 25 | /* @var $APPLICATION CMain */ 26 | $APPLICATION->ThrowException("Action `$actionFile` not exists"); 27 | } 28 | require_once($_SERVER["DOCUMENT_ROOT"] . "/bitrix/modules/main/include/epilog_admin_after.php"); 29 | ?> -------------------------------------------------------------------------------- /admin/createScenario.php: -------------------------------------------------------------------------------- 1 | getContext()->getRequest(); 8 | $fileName = ''; 9 | $hasError = false; 10 | if ($_POST['save'] != "" && $_POST['name']) { 11 | $name = trim($request->get('name')); 12 | $priority = trim($request->get('priority')); 13 | $time = trim($request->get('time')); 14 | 15 | try { 16 | $fileName = $module->createScenario($name, $priority, $time); 17 | } catch (Exception $e) { 18 | $hasError = true; 19 | } 20 | } 21 | 22 | if ($_POST['save'] != "" && !$_POST['name']) { 23 | $hasError = true; 24 | } 25 | $APPLICATION->SetTitle($localization->getDataByPath('title')); 26 | require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_admin_after.php"); 27 | 28 | $fileName && CAdminMessage::ShowNote($localization->message('path-to-file', array('#path#' => $fileName))); 29 | $hasError && CAdminMessage::ShowMessage(array("MESSAGE" => $localization->message('save-file-error'), "TYPE" => "ERROR")); 30 | ?>
"edit1", 34 | "TAB" => $localization->getDataByPath('title'), 35 | "ICON" => "iblock", 36 | "TITLE" => $localization->getDataByPath('title'), 37 | ) 38 | )); 39 | 40 | $form->SetShowSettings(false); 41 | $form->Begin(array( 42 | 'FORM_ACTION' => $APPLICATION->GetCurUri() 43 | )); 44 | $form->BeginNextFormTab(); 45 | $form->AddEditField('name', $localization->message('field.name'), true, array('size' => 80)); 46 | 47 | $priorities = array(); 48 | foreach (\WS\ReduceMigrations\Scenario\ScriptScenario::getPriorities() as $priority) { 49 | $priorities[$priority] = $localization->message('priority.' . $priority); 50 | } 51 | 52 | $form->AddDropDownField('priority', $localization->message('field.priority'), true, $priorities); 53 | $form->AddEditField('time', $localization->message('field.time'), false, array('size' => 3)); 54 | 55 | $form->EndTab(); 56 | $form->Buttons(array('btnSave' => false, 'btnApply' => false)); 57 | $form->sButtonsContent = ''; 58 | $form->Show(); 59 | ?>
60 | SetTitle($localization->getDataByPath('title')); 13 | $sTableID = "ws_reducemigrations_log_table"; 14 | $oSort = new CAdminSorting($sTableID, "date", "asc"); 15 | $lAdmin = new CAdminList($sTableID, $oSort); 16 | 17 | $arHeaders = array( 18 | array("id" => "updateDate", "content" => $localization->getDataByPath('fields.updateDate'), "default"=>true), 19 | array("id" => "description", "content" => $localization->getDataByPath('fields.description'), "default" => true), 20 | array("id" => "hash", "content" => $localization->getDataByPath('fields.hash'), "default" => true), 21 | array("id" => "dispatcher", "content" => $localization->getDataByPath('fields.dispatcher'), "default" => true) 22 | ); 23 | $lAdmin->AddHeaders($arHeaders); 24 | 25 | $models = AppliedChangesLogModel::find(array( 26 | 'limit' => 500, 27 | 'order' => array( 28 | 'groupLabel' => 'desc' 29 | ) 30 | )); 31 | 32 | $rowsData = array(); 33 | array_walk($models, function (AppliedChangesLogModel $model) use (& $rowsData) { 34 | $row = & $rowsData[$model->getGroupLabel()]; 35 | if(!$row) { 36 | $row = array( 37 | 'label' => $model->getGroupLabel(), 38 | 'updateDate' => $model->getDate()->format('d.m.Y H:i:s'), 39 | 'hash' => $model->getHash(), 40 | 'dispatcher' => $model->getSetupLog() ? $model->getSetupLog()->shortUserInfo() : '' 41 | ); 42 | } 43 | $row['description'] = $row['description'] ? implode("
", array($row['description'], $model->getName())) : $model->getName(); 44 | if ($model->isFailed()) { 45 | $row['error'][] = array( 46 | 'data' => array('message' => $model->getErrorMessage()), 47 | 'id' => $model->getId() 48 | ); 49 | } 50 | }); 51 | 52 | $rsData = new CAdminResult(null, $sTableID); 53 | $rsData->InitFromArray($rowsData); 54 | $rsData->NavStart(); 55 | 56 | $lAdmin->NavText($rsData->GetNavPrint($localization->getDataByPath('messages.pages'))); 57 | while ($rowData = $rsData->NavNext()) { 58 | $row = $lAdmin->AddRow($rowData['label'], $rowData); 59 | $description = $rowData['description']; 60 | if ($rowData['error']) { 61 | $description = array(); 62 | foreach ($rowData['error'] as $rowErrorData) { 63 | $description[] = ''.$rowErrorData['data']['message'].''; 64 | } 65 | $description = implode('
', $description); 66 | } 67 | 68 | $row->AddViewField('description', $description); 69 | $row->AddActions(array( 70 | array( 71 | "ICON" => "view", 72 | "TEXT" => $localization->message('messages.view'), 73 | "ACTION" => $lAdmin->ActionRedirect("ws_reducemigrations.php?q=detail&label=".$rowData['label'].'&type=applied'. '&lang=' . LANGUAGE_ID), 74 | "DEFAULT" => true 75 | ) 76 | )); 77 | } 78 | if ($_REQUEST["mode"] == "list") { 79 | require($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_admin_js.php"); 80 | } else { 81 | require_once($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/main/include/prolog_admin_after.php"); 82 | } 83 | 84 | $lAdmin->CheckListMode(); 85 | $lAdmin->DisplayList(); 86 | ?> 87 | 103 | isAdmin()) { 4 | return array(); 5 | } 6 | CModule::IncludeModule('ws.reducemigrations'); 7 | $loc = \WS\ReduceMigrations\Module::getInstance()->getLocalization('menu'); 8 | $inputUri = '/bitrix/admin/ws_reducemigrations.php?lang=' . LANGUAGE_ID . '&q='; 9 | return array( 10 | array( 11 | 'parent_menu' => 'global_menu_settings', 12 | 'sort' => 500, 13 | 'text' => $loc->getDataByPath('title'), 14 | 'title' => $loc->getDataByPath('title'), 15 | 'module_id' => 'ws_reducemigrations', 16 | 'icon' => '', 17 | 'items_id' => 'ws_reducemigrations_menu', 18 | 'items' => array( 19 | array( 20 | 'text' => $loc->getDataByPath('apply'), 21 | 'url' => $inputUri.'main', 22 | ), 23 | array( 24 | 'text' => $loc->getDataByPath('createScenario'), 25 | 'url' => $inputUri.'createScenario' 26 | ), 27 | array( 28 | 'text' => $loc->getDataByPath('log'), 29 | 'url' => $inputUri.'log', 30 | 'more_url' => array($inputUri.'detail') 31 | ) 32 | ) 33 | ) 34 | ); 35 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worksolutions/bitrix-reduce-migrations", 3 | "description": "Bitrix module for migrations", 4 | "type": "bitrix-module", 5 | "license": "MIT", 6 | "support": { 7 | "issues": "https://github.com/worksolutions/bitrix-reduce-migrations/issues", 8 | "source": "https://github.com/worksolutions/bitrix-reduce-migrations" 9 | }, 10 | "authors": [ 11 | { 12 | "name": "Igor Pomiluyko", 13 | "email": "pomiluyko@worksolutions.ru" 14 | } 15 | ], 16 | "extra": { 17 | "installer-name": "ws.reducemigrations" 18 | }, 19 | "require": { 20 | "php": ">=5.3.0", 21 | "composer/installers": "~1" 22 | }, 23 | "scripts": { 24 | "post-install-cmd": [ 25 | "php scripts/install.php" 26 | ] 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /data/scenarioTemplate.tpl: -------------------------------------------------------------------------------- 1 | printer() method. 39 | **/ 40 | public function commit() { 41 | // my code 42 | } 43 | 44 | /** 45 | * Write action by rollback scenario. Use method `getData` for getting commit saved data. 46 | * For printing info into console use object from $this->printer() method. 47 | **/ 48 | public function rollback() { 49 | // my code 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /description.ru: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/description.ru -------------------------------------------------------------------------------- /docs/cli.md: -------------------------------------------------------------------------------- 1 | ##### [Главная страница](../README.md) 2 | 3 | ### Интерфейс командной строки 4 | 5 | Основным требованием к любому функуионалу должно быть требование удобства использования. Это означает что: 6 | 7 | 1. Все действия должны быть интуитивно понятны; 8 | 2. Работая с функционалом нет нужды обращаться к документации. 9 | 10 | Мы постарались сделать наиболее удобным использование модуля как при написании миграции так и при работе с очередью уже готовых миграций. 11 | 12 | Основной интерфейс модуля - терминал. При использовании этого подхода есть неоспоримые приимущества: 13 | 14 | 1. Время выполнения миграций не ограничено таймаутом сервера; 15 | 2. Возможность автоматизировать обновление миграций совместно с обновлением кода. К примеру, можно интегрировать механизм обновления миграций 16 | при помощи функционала перехватчиков `СУРВ Git` 17 | 18 | #### Начало работ 19 | 20 | Файл для работы с миграцими через интерфейс командной строки расположен по пути `bitrix/tools/migrate`, таким образом запускать миграции через командную строку всегда нужно через этот файл. 21 | 22 | Пример: 23 | 24 | ```sh 25 | $ php bitrix/tools/migrate 26 | ``` 27 | 28 | Отображает список доступных действий 29 | 30 | ![Помощь](img/cli_help.png) 31 | 32 | Можно получить более подробную информацию по каждому действию набрав ```--help``` после имени действия, к примеру: 33 | ```sh 34 | $ php bitrix/tools/migrate create --help 35 | ``` 36 | 37 | ![Справка](img/cli_action_help.png) 38 | Выводится простая, но в то же время содержательная справка по параметрам действия и их использованию 39 | 40 | #### Список миграций доступных для обновления 41 | 42 | ```sh 43 | $ php bitrix/tools/migrate list 44 | ``` 45 | 46 | ![Список подготовленных миграций](img/cli_list.png) 47 | 48 | Миграции в списке группируются по приоритету, именно в таком порядке они будут обновляться. Так же выводится суммарное время обновления согласно пустановленным периодам в сценариях. Это позволяет правильно ориентироваться по времени при обновлении. 49 | 50 | #### Создание миграции 51 | 52 | Интерактивный способ 53 | 54 | ```sh 55 | $ php bitrix/tools/migrate create 56 | ``` 57 | 58 | ![Список подготовленных миграций](img/cli_create_full.png) 59 | 60 | Быстрый способ 61 | 62 | ![Список подготовленных миграций](img/cli_create_quick.png) 63 | 64 | #### Применение подготовленных миграций 65 | 66 | Запуск миграции по хешу. 67 | 68 | ```sh 69 | $ php bitrix/tools/migrate apply a5468917 70 | ``` 71 | 72 | ![Список подготовленных миграций](img/cli_apply_hash.png) 73 | 74 | Из списка подготовленных миграций выбирается та, которая соответствует подстроке хеша. 75 | 76 | Запуск всех миграций. 77 | 78 | ```sh 79 | $ php bitrix/tools/migrate apply -f 80 | ``` 81 | 82 | ![Список подготовленных миграций](img/cli_apply_all.png) 83 | 84 | Флаг ```-f``` указывает на то что применение миграций начинать без запроса на подтверждение. 85 | 86 | Запуск всех миграций. 87 | 88 | ```sh 89 | $ php bitrix/tools/migrate apply -f --skip-optional 90 | ``` 91 | 92 | ![Список подготовленных миграций](img/cli_apply_all.png) 93 | 94 | Параметр ```--skip-optional``` указывает на пропуск опциональных миграций. Опциональные миграции создаются в основном для актуализаций боевой площадки и занимают продолжительное время, на площадках разработчиков такая актуализация не требуется и при обновлении миграций на площадке разработчика их можно пропустить. 95 | 96 | Подойдет для обработчиков ```git``` 97 | 98 | #### История применения миграций 99 | 100 | Все примененные миграции сохраняются в истории. 101 | 102 | Последнее обновление миграций: 103 | ```sh 104 | $ php bitrix/tools/migrate history 105 | ``` 106 | 107 | ![Список подготовленных миграций](img/cli_history_last.png) 108 | 109 | Просмотр истории по количеству последних миграций: 110 | ```sh 111 | $ php bitrix/tools/migrate history 112 | ``` 113 | 114 | ![Список подготовленных миграций](img/cli_history_count.png) 115 | 116 | #### Откат миграций 117 | 118 | Откат миграций производится через метод класса миграций ```rollback``` 119 | 120 | Откат последнего обновления: 121 | ```sh 122 | $ php bitrix/tools/migrate rollback 123 | ``` 124 | 125 | ![Список подготовленных миграций](img/cli_rollback_last.png) 126 | 127 | Откат последнего обновления: 128 | ```sh 129 | $ php bitrix/tools/migrate rollback 130 | ``` 131 | 132 | ![Список подготовленных миграций](img/cli_rollback_last.png) 133 | 134 | Откат одной миграции по хешу: 135 | ```sh 136 | $ php bitrix/tools/migrate rollback b907dc51 137 | ``` 138 | 139 | ![Список подготовленных миграций](img/cli_rollback_hash.png) 140 | 141 | Откат определенного количества последних миграций: 142 | ```sh 143 | $ php bitrix/tools/migrate rollback --count=2 144 | ``` 145 | 146 | ![Список подготовленных миграций](img/cli_rollback_count.png) 147 | 148 | Откат всех свежих миграций вплоть до определенной из списка: 149 | Откат определенного количества последних миграций: 150 | ```sh 151 | $ php bitrix/tools/migrate rollback --count=2 152 | ``` 153 | 154 | ![Список подготовленных миграций](img/cli_rollback_up_to.png) 155 | 156 | Применение и откат миграций сопровождается списком с перечнем того что будет обновляться или отменяться. 157 | -------------------------------------------------------------------------------- /docs/img/cli_action_help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_action_help.png -------------------------------------------------------------------------------- /docs/img/cli_apply_all.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_apply_all.png -------------------------------------------------------------------------------- /docs/img/cli_apply_hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_apply_hash.png -------------------------------------------------------------------------------- /docs/img/cli_apply_skip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_apply_skip.png -------------------------------------------------------------------------------- /docs/img/cli_create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_create.png -------------------------------------------------------------------------------- /docs/img/cli_create_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_create_full.png -------------------------------------------------------------------------------- /docs/img/cli_create_quick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_create_quick.png -------------------------------------------------------------------------------- /docs/img/cli_help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_help.png -------------------------------------------------------------------------------- /docs/img/cli_history_count.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_history_count.png -------------------------------------------------------------------------------- /docs/img/cli_history_last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_history_last.png -------------------------------------------------------------------------------- /docs/img/cli_list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_list.jpg -------------------------------------------------------------------------------- /docs/img/cli_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_list.png -------------------------------------------------------------------------------- /docs/img/cli_rollback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_rollback.jpg -------------------------------------------------------------------------------- /docs/img/cli_rollback_count.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_rollback_count.png -------------------------------------------------------------------------------- /docs/img/cli_rollback_hash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_rollback_hash.png -------------------------------------------------------------------------------- /docs/img/cli_rollback_last.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_rollback_last.png -------------------------------------------------------------------------------- /docs/img/cli_rollback_up_to.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/cli_rollback_up_to.png -------------------------------------------------------------------------------- /docs/img/create_scenario.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/create_scenario.png -------------------------------------------------------------------------------- /docs/img/install_market.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/install_market.jpg -------------------------------------------------------------------------------- /docs/img/main.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/main.jpg -------------------------------------------------------------------------------- /docs/img/project_state.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/project_state.png -------------------------------------------------------------------------------- /docs/img/settings_page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/settings_page.jpg -------------------------------------------------------------------------------- /docs/img/update_scenario.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/docs/img/update_scenario.jpg -------------------------------------------------------------------------------- /docs/scripts.md: -------------------------------------------------------------------------------- 1 | ##### [Главная страница](../README.md) 2 | 3 | ## Работа со скриптами миграций 4 | 5 | `Модуль миграций` предоставляет функционал создания "гибкой миграции" путем определения сценария обновления программистом. Также имеется набор классов для быстрого и удобного создания новых данных. 6 | 7 | Файлы миграций создаются в виде PHP классов. 8 | 9 | ### 1. Создание класса сценария миграции 10 | 11 | #### Консоль 12 | 13 | Один из простых способов создания миграйции – через консоль. 14 | 15 | ![Создание сценария из консоли](img/cli_create.png) 16 | 17 | В этом случае необходимо ввести название миграции и приоритет. 18 | 19 | Существуют три варианта проритета миграции: 20 | * ```high (h)``` - высокий, сценарии с этим приоритетом обновляются в первую очередь; 21 | * ```middle (m)``` - средний, такие сценарии выполняются сразу после сценариев с высоким приоритетом; 22 | * ```optional (o)``` - опциональный, могут быть пропушены во время обновления; 23 | 24 | В любом случае приоритет можно сменить в исходном коде миграции после ее создания. 25 | 26 | #### Административный интерфейс 27 | 28 | Создание класса сценария осуществляется из меню `Миграции данных -> Сценарий обновления`, где необходимо ввести название сценария, выбрать приоритет миграции и примерное время выполнения. 29 | 30 | Примерное время выполнения можно установить непосредственно в исходном коде файла миграции. 31 | 32 | ##### Создание сценария. Ввод названия 33 | 34 | ![Создание сценария. Ввод названия](img/create_scenario.png) 35 | 36 | Название можно задавать кириллицей. После создания появляется сообщение с информацией о местонахождении файла класса 37 | 38 | ![Создание сценария. Ввод названия](img/update_scenario.jpg) 39 | 40 | ### 2. Редактирование. Определение алгоритма исполнения сценария 41 | 42 | По пути, указанному в сообщении находится следующий класс: 43 | 44 | ##### Сценарий. Редактирование класса 45 | 46 | ```php 47 | createIblock('type_content', 'Новости', function (\WS\ReduceMigrations\Builder\Entity\Iblock $iblock) { 125 | $iblock 126 | ->siteId('s1') 127 | ->sort(100) 128 | ->code('news') 129 | ->groupId(array( 130 | '2' => 'R' 131 | )); 132 | $iblock 133 | ->addProperty('Выводить на главной') 134 | ->code('showOnMain') 135 | ->typeCheckbox() 136 | ->addEnum('да'); 137 | }); 138 | 139 | ``` 140 | 141 | Перечень классов: 142 | 143 | * \WS\ReduceMigrations\Builder\TableBuilder 144 | * \WS\ReduceMigrations\Builder\IblockBuilder 145 | * \WS\ReduceMigrations\Builder\HighLoadBlockBuilder 146 | * \WS\ReduceMigrations\Builder\FormBuilder 147 | * \WS\ReduceMigrations\Builder\EventsBuilder 148 | * \WS\ReduceMigrations\Builder\AgentBuilder 149 | -------------------------------------------------------------------------------- /docs/setup.md: -------------------------------------------------------------------------------- 1 | ##### [Главная страница](../README.md) 2 | 3 | # Установка и настройка 4 | 5 | ## Composer 6 | 7 | Подключение модуля необходимо проводить находясь в DOCUMENT_ROOT проекта: 8 | 9 | ``` 10 | composer require worksolutions/bitrix-reduce-migrations 11 | ``` 12 | Команда перенесет файлы модуля в папку `/bitrix/modules/ws.reducemigrations` 13 | 14 | Следующим шагом будет регистрация в битрикс: 15 | ``` 16 | composer run-script post-install-cmd -d bitrix/modules/ws.reducemigrations 17 | ``` 18 | 19 | Результатом регистрации будут следующие пункты: 20 | 1. Модуль будет зарегистрирован в системе; 21 | 2. Будет создан каталог ```reducemigrations``` в папке на уровне bitrix; 22 | 3. В каталог bitrix/admin переносится файл-контроллер модуля. Для работы модуля через административный интерфейс. 23 | 4. Появится файл ```bitrix/tools/migrate``` для работы миграций через командную строку. 24 | 25 | Также регистрацию модуля можно запустить через страницу "Установленные решения": 26 | ``` 27 | http://my-site.ru/bitrix/admin/partner_modules.php?lang=ru 28 | ``` 29 | 30 | ## Marketplace 31 | 32 | Для установки в адресную строку браузера, после доменного имени, прописать: 33 | ``` 34 | /bitrix/admin/update_system_partner.php?addmodule=ws.reducemigrations 35 | ``` 36 | 37 | К примеру: 38 | ``` 39 | http://my-site.ru/bitrix/admin/update_system_partner.php?addmodule=ws.reducemigrations 40 | ``` 41 | 42 | При установке модуля необходимо определить основные параметры. 43 | 44 | ### Определение параметров при установке модуля 45 | 46 | Для успешной работы модуля необходимо определить каталог, где будут создаваться файлы синхронизации данных. 47 | Каталог сделать должен синхронизироваться системой версионирования проекта. 48 | 49 | ![Ввод параметров при установке модуля.](img/install_market.jpg) 50 | 51 | ### Настройка модуля 52 | 53 | После установки модуля в настройках можно изменить путь к каталогу, в котором будут храниться миграции 54 | ![Настройка модуля.](img/settings_page.jpg) 55 | 56 | ## Переход с первой версии модуля 57 | 58 | Если на проекте использовалась первая [версия модуля](https://github.com/worksolutions/bitrix-module-migrations), то желательно для нового модуля настроить другой каталог хранения файлов миграций. Это исключит путаницу связанную с одинаковыми по назначению, но не по реализации, модулями. Конечно при этом все члены команды должны осознавать то, что они работают с новой версией миграций данных. 59 | 60 | Таким образом переход к новому модулю можно осуществить в два этапа: 61 | 62 | 1. Установить параллельно новый модуль; 63 | 2. Через время удалить неиспользуемый, в тот момент, когда полностью налажена работа с новым модулем. 64 | -------------------------------------------------------------------------------- /docs/web.md: -------------------------------------------------------------------------------- 1 | ##### [Главная страница](../README.md) 2 | 3 | ### Административный интерфейс модуля 4 | 5 | Хотя модуль имеет возможность работать через административный интерфейс, рекомендуется использовать его только для чтения данных о миграциях. 6 | 7 | ![Применение сценариев миграций](img/main.jpg) 8 | -------------------------------------------------------------------------------- /include.php: -------------------------------------------------------------------------------- 1 | getLocalization('setup'); 4 | $options = \WS\ReduceMigrations\Module::getInstance()->getOptions(); 5 | $module = \WS\ReduceMigrations\Module::getInstance(); 6 | 7 | $errors && CAdminMessage::ShowMessage( 8 | array( 9 | "MESSAGE" => implode(', ', $errors), 10 | "TYPE" => "ERROR", 11 | ) 12 | ); 13 | 14 | $form = new CAdminForm('ew', array( 15 | array( 16 | 'DIV' => 't1', 17 | 'TAB' => $localization->getDataByPath('tab'), 18 | ), 19 | )); 20 | 21 | echo BeginNote(); 22 | echo $localization->getDataByPath('description'); 23 | echo EndNote(); 24 | 25 | $form->Begin(array( 26 | 'FORM_ACTION' => $APPLICATION->GetCurUri(), 27 | )); 28 | $form->BeginNextFormTab(); 29 | $form->AddEditField('data[catalog]', $localization->getDataByPath('fields.catalog'), true, array(), $options->catalogPath ?: '/bitrix/php_interface/reducemigrations'); 30 | 31 | 32 | $form->Buttons(array('btnSave' => false, 'btnApply' => true)); 33 | $form->Show(); 34 | -------------------------------------------------------------------------------- /install/tools/migrate: -------------------------------------------------------------------------------- 1 | getLocalization('uninstall'); 4 | $options = \WS\ReduceMigrations\Module::getInstance()->getOptions(); 5 | $form = new CAdminForm('ew', array( 6 | array( 7 | 'DIV' => 't1', 8 | 'TAB' => $localization->getDataByPath('tab'), 9 | ) 10 | )); 11 | ShowMessage(array( 12 | 'MESSAGE' => $localization->getDataByPath('description'), 13 | 'TYPE' => 'OK' 14 | )); 15 | 16 | $errors && ShowError(implode(', ', $errors)); 17 | $form->Begin(array( 18 | 'FORM_ACTION' => $APPLICATION->GetCurUri() 19 | )); 20 | $form->BeginNextFormTab(); 21 | $form->AddCheckBoxField('data[removeAll]', $localization->getDataByPath('fields.removeAll'), true, "Y", false); 22 | $form->BeginCustomField('data[remove]', ''); 23 | ?> 24 | 25 | 26 | 27 | 28 | EndCustomField('data[remove]'); 30 | $form->Buttons(array('btnSave' => false, 'btnApply' => true)); 31 | $form->Show(); -------------------------------------------------------------------------------- /install/version.php: -------------------------------------------------------------------------------- 1 | "1.1.0", 4 | "VERSION_DATE" => "2019-09-07 15:12:00" 5 | ); 6 | -------------------------------------------------------------------------------- /lang/en/admin.php: -------------------------------------------------------------------------------- 1 | array( 4 | 'title' => 'Migrations management', 5 | 'list' => array( 6 | 'scenarios' => 'New scenarios' 7 | ), 8 | 'priority' => array( 9 | 'priority' => 'Migration priority:', 10 | 'high' => 'High:', 11 | 'medium' => 'Medium:', 12 | 'optional' => 'Optional:', 13 | ), 14 | 'skipOptional' => 'Skip migrations with type "Optional"', 15 | 'errorList' => 'Unsuccessful applied migrations', 16 | 'appliedList' => 'Successful applied migrations', 17 | 'approximatelyTime' => 'Approximately time of migrations', 18 | 'timeLang' => [ 19 | 'minutes' => 'min', 20 | 'seconds' => 'sec' 21 | ], 22 | 'btnRollback' => 'Undo last change', 23 | 'btnApply' => 'Apply', 24 | 'lastSetup' => array( 25 | 'sectionName' => 'Last update :time: - :user:' 26 | ), 27 | 'common' => array( 28 | 'listEmpty' => 'List is empty', 29 | 'reference-fix' => 'References synchronizing', 30 | 'pageEmpty' => 'Data for update don`t exists yet' 31 | ), 32 | 'newChangesDetail' => 'Changes list', 33 | 'newChangesTitle' => 'New changes', 34 | 'errorWindow' => 'Error info', 35 | 'diagnostic' => 'Errors diagnosis, the use of the migration is possible only after the correction', 36 | 'platformVersion' => array( 37 | 'ok' => 'Platform version', 38 | 'error' => 'Incorrect platform version', 39 | 'setup' => 'Setup', 40 | ) 41 | ), 42 | 'applyError' => array( 43 | 'message' => 'Message', 44 | 'data' => 'Data', 45 | 'trace' => 'Call stack', 46 | 'error' => array( 47 | 'modelNotExists' => 'Data for record id =: id: does not exist' 48 | ) 49 | ), 50 | 'createScenario' => array( 51 | 'title' => 'The script scenario', 52 | 'field' => array( 53 | 'name' => 'Title', 54 | 'priority' => 'Priority', 55 | 'time' => 'Approximately migration time(seconds)', 56 | ), 57 | 'priority' => array( 58 | 'high' => 'High', 59 | 'medium' => 'Medium', 60 | 'optional' => 'Optional', 61 | ), 62 | 'path-to-file' => 'Class file migration is #path#', 63 | 'save-file-error' => 'An error occured save file', 64 | 'button' => array( 65 | 'create' => 'Create migration scenario' 66 | ) 67 | ), 68 | 'log' => array( 69 | 'title' => 'Updates log', 70 | 'fields' => array( 71 | 'updateDate' => 'Date', 72 | 'description' => 'Update features', 73 | 'hash' => 'Migration hash', 74 | 'dispatcher' => 'Update by' 75 | ), 76 | 'messages' => array( 77 | 'InsertReference' => 'Insert other reference', 78 | 'view' => 'Analysis of changes', 79 | 'pages' => 'Pages', 80 | 'actualization' => 'Actualization sources', 81 | 'descriptionMoreLink' => 'detail', 82 | 'errorWindow' => 'Error information' 83 | ) 84 | ), 85 | 'detail' => array( 86 | 'title' => '#date. #source. Update by - #deployer', 87 | 'tabs' => array( 88 | 'diff' => 'Features', 89 | 'final' => 'Update result', 90 | 'merge' => 'Data before update' 91 | ), 92 | 'message' => array( 93 | 'nobody' => 'The site is not updated', 94 | 'show' => 'show data', 95 | 'hide' => 'hide data', 96 | ), 97 | 'serviceLabels' => array( 98 | '~reference' => 'HASH', 99 | '~property_list_values' => 'VALUES', 100 | 'Reference fix' => 'Register links with the essence of the platform', 101 | 'Insert reference' => 'The new entity reference', 102 | 'reference' => 'HASH', 103 | 'group' => 'Group entity ( the handler )', 104 | 'dbVersion' => 'Platform version' 105 | ) 106 | ), 107 | 'cli' => array( 108 | 'common' => array( 109 | 'reference-fix' => 'References synchronizing' 110 | ), 111 | ), 112 | ); -------------------------------------------------------------------------------- /lang/en/info.php: -------------------------------------------------------------------------------- 1 | 'Data migrations', 4 | 'description' => 'Allows you to synchronize data from different copies of site', 5 | 'partner' => array( 6 | 'name' => 'WorkSolutions', 7 | 'url' => 'http://www.worksolutions.ru' 8 | ) 9 | ); -------------------------------------------------------------------------------- /lang/en/menu.php: -------------------------------------------------------------------------------- 1 | 'Reduce Migrations', 5 | 'apply' => 'Update', 6 | 'log' => 'Log', 7 | 'createScenario' => 'Scenario', 8 | 'diagnostic' => 'Diagnostic' 9 | ); -------------------------------------------------------------------------------- /lang/en/setup.php: -------------------------------------------------------------------------------- 1 | 'Installing the Data migrations module', 4 | 'name' => 'WS ReduceMigrations', 5 | 'tab' => 'Settings', 6 | 'description' => 'You must specify the directory location of the migration file. relative to the root directory of the project. ($ SERVER [\'DOCUMENT_ROOT\']) ', 7 | 'fields' => array( 8 | 'catalog' => 'Directory path', 9 | 'apply' => 'Set Up' 10 | ), 11 | 'errors' => array( 12 | 'notCreateDir' => 'Unable to create directory', 13 | 'catalogFieldEmpty' => 'Catalog field is empty' 14 | ), 15 | 'section' => array( 16 | 'disableHandlers' => 'Use synchronization' 17 | ) 18 | ); -------------------------------------------------------------------------------- /lang/en/tests.php: -------------------------------------------------------------------------------- 1 | array( 4 | 'name' => 'WorkSolutions. Reduce Migrations module', 5 | 'report' => array( 6 | 'completed' => 'Completed', 7 | 'assertions' => 'Assertions' 8 | ) 9 | ), 10 | 'cases' => array( 11 | \WS\ReduceMigrations\Tests\Cases\IblockBuilderCase::className() => array( 12 | 'name' => 'IblockBuilder test', 13 | 'description' => '', 14 | 'errors' => array( 15 | ) 16 | ), 17 | \WS\ReduceMigrations\Tests\Cases\HighLoadBlockBuilderCase::className() => array( 18 | 'name' => 'HighLoadBlockBuilder', 19 | 'description' => '', 20 | 'errors' => array( 21 | ) 22 | ), 23 | \WS\ReduceMigrations\Tests\Cases\AgentBuilderCase::className() => array( 24 | 'name' => 'AgentBuilder', 25 | 'description' => '', 26 | 'errors' => array( 27 | ) 28 | ), 29 | \WS\ReduceMigrations\Tests\Cases\EventsBuilderCase::className() => array( 30 | 'name' => 'EventsBuilder', 31 | 'description' => '', 32 | 'errors' => array( 33 | ) 34 | ), 35 | \WS\ReduceMigrations\Tests\Cases\FormBuilderCase::className() => array( 36 | 'name' => 'FormBuilder', 37 | 'description' => '', 38 | 'errors' => array( 39 | ) 40 | ), 41 | \WS\ReduceMigrations\Tests\Cases\TableBuilderCase::className() => array( 42 | 'name' => 'TableBuilder', 43 | 'description' => '', 44 | 'errors' => array( 45 | ) 46 | ), 47 | ) 48 | ); -------------------------------------------------------------------------------- /lang/en/uninstall.php: -------------------------------------------------------------------------------- 1 | 'Deleting data of Reduce Migrations module', 4 | 'tab' => 'Delete options', 5 | 'description' => "When you delete data , the project will not be able to work with the current migrations.\n" 6 | ."You will need to initialize the mechanism of migration once again , starting with the current version .\n" 7 | ."Information to remove : tables migration , customization , migration catalog (if used versioning system" 8 | ."should register changes )", 9 | 'fields' => array( 10 | 'removeAll' => 'Erase migration data' 11 | ) 12 | ); -------------------------------------------------------------------------------- /lang/ru/admin.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/lang/ru/admin.php -------------------------------------------------------------------------------- /lang/ru/info.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/lang/ru/info.php -------------------------------------------------------------------------------- /lang/ru/menu.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/lang/ru/menu.php -------------------------------------------------------------------------------- /lang/ru/setup.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/lang/ru/setup.php -------------------------------------------------------------------------------- /lang/ru/tests.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/lang/ru/tests.php -------------------------------------------------------------------------------- /lang/ru/uninstall.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/lang/ru/uninstall.php -------------------------------------------------------------------------------- /lib/applyresult.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\ReduceMigrations; 7 | 8 | 9 | class ApplyResult { 10 | private $success; 11 | private $message; 12 | private $id; 13 | 14 | /** 15 | * @param string $value 16 | * @return $this 17 | */ 18 | public function setMessage($value) { 19 | $this->message = $value; 20 | return $this; 21 | } 22 | 23 | /** 24 | * @return string 25 | */ 26 | public function getMessage() { 27 | return $this->message; 28 | } 29 | 30 | /** 31 | * @return boolean 32 | */ 33 | public function isSuccess() { 34 | return $this->success; 35 | } 36 | 37 | /** 38 | * @param boolean $value 39 | * @return $this 40 | */ 41 | public function setSuccess($value) { 42 | $this->success = $value; 43 | return $this; 44 | } 45 | 46 | /** 47 | * @return mixed 48 | */ 49 | public function getId() { 50 | return $this->id; 51 | } 52 | 53 | /** 54 | * @param mixed $id 55 | * @return $this 56 | */ 57 | public function setId($id) { 58 | $this->id = $id; 59 | return $this; 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /lib/builder/agentbuilder.php: -------------------------------------------------------------------------------- 1 | commit($agent); 23 | return $agent; 24 | } 25 | 26 | /** 27 | * @param string $agentFunction 28 | * @param \Closure $callback 29 | * @return Agent 30 | * @throws BuilderException 31 | */ 32 | public function updateAgent($agentFunction, $callback) { 33 | $data = $this->findAgent($agentFunction); 34 | $agent = new Agent($agentFunction); 35 | $agent->setId($data['ID']); 36 | $agent->markClean(); 37 | $callback($agent); 38 | $this->commit($agent); 39 | return $agent; 40 | } 41 | 42 | /** 43 | * @var Agent $agent 44 | * @throws BuilderException 45 | */ 46 | private function commit($agent) { 47 | global $DB, $APPLICATION; 48 | $DB->StartTransaction(); 49 | $gw = new \CAgent(); 50 | try { 51 | if ($agent->getId() > 0) { 52 | if ($agent->isDirty()) { 53 | $res = $gw->Update($agent->getId(), $agent->getData()); 54 | if (!$res) { 55 | throw new BuilderException("Agent wasn't updated"); 56 | } 57 | } 58 | } else { 59 | $res = $gw->AddAgent( 60 | $agent->getAttribute('NAME'), 61 | $agent->getAttribute('MODULE_ID'), 62 | $agent->getAttribute('IS_PERIOD'), 63 | $agent->getAttribute('AGENT_INTERVAL'), 64 | '',//bitrix doesn't use this parameter 65 | $agent->getAttribute('ACTIVE'), 66 | $agent->getAttribute('NEXT_EXEC'), 67 | $agent->getAttribute('SORT'), 68 | $agent->getAttribute('USER_ID') 69 | ); 70 | if (!$res) { 71 | throw new BuilderException("Agent wasn't created: " . $APPLICATION->GetException()->GetString()); 72 | } 73 | $agent->setId($res); 74 | } 75 | 76 | } catch (BuilderException $e) { 77 | $DB->Rollback(); 78 | throw new BuilderException($e->getMessage()); 79 | } 80 | $DB->Commit(); 81 | } 82 | 83 | /** 84 | * @param $callback 85 | * @return array 86 | * @throws BuilderException 87 | */ 88 | private function findAgent($callback) { 89 | $agent = \CAgent::GetList(null, array( 90 | 'NAME' => $callback, 91 | ))->Fetch(); 92 | if (empty($agent)) { 93 | throw new BuilderException("Agent {$callback} not found"); 94 | } 95 | return $agent; 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /lib/builder/builderexception.php: -------------------------------------------------------------------------------- 1 | callback($callback); 26 | } 27 | 28 | public function getMap() { 29 | return array( 30 | 'sort' => 'SORT', 31 | 'active' => 'ACTIVE', 32 | 'module' => 'MODULE_ID', 33 | 'callback' => 'NAME', 34 | 'userId' => 'USER_ID', 35 | 'isPeriod' => 'IS_PERIOD', 36 | 'interval' => 'AGENT_INTERVAL', 37 | 'nextExec' => 'NEXT_EXEC', 38 | ); 39 | } 40 | 41 | /** 42 | * @param int $id 43 | * @return Agent 44 | */ 45 | public function setId($id) { 46 | $this->id = $id; 47 | return $this; 48 | } 49 | 50 | /** 51 | * @return int 52 | */ 53 | public function getId() { 54 | return $this->id; 55 | } 56 | 57 | public static function create($callback) { 58 | $agent = new Agent($callback); 59 | $agent 60 | ->sort(self::DEFAULT_SORT) 61 | ->active(true) 62 | ->interval(self::DEFAULT_INTERVAL) 63 | ->isPeriod(false) 64 | ->nextExec(new DateTime()); 65 | 66 | return $agent; 67 | } 68 | 69 | /** 70 | * @param bool $active 71 | * @return Agent 72 | */ 73 | public function active($active) { 74 | $this->setAttribute('ACTIVE', $active ? 'Y' : 'N'); 75 | return $this; 76 | } 77 | 78 | /** 79 | * @param bool $isPeriod 80 | * @return Agent 81 | */ 82 | public function isPeriod($isPeriod) { 83 | $this->setAttribute('IS_PERIOD', $isPeriod ? 'Y' : 'N'); 84 | return $this; 85 | } 86 | 87 | } -------------------------------------------------------------------------------- /lib/builder/entity/base.php: -------------------------------------------------------------------------------- 1 | params[$attributeName] = $value; 19 | $this->markDirty(); 20 | return $this; 21 | } 22 | 23 | /** 24 | * @param string $name 25 | * 26 | * @return mixed 27 | */ 28 | public function getAttribute($name) { 29 | return $this->params[$name]; 30 | } 31 | /** 32 | * @return array 33 | */ 34 | public function getData() { 35 | return $this->params; 36 | } 37 | 38 | protected abstract function getMap(); 39 | 40 | public function __call($name, $arguments) { 41 | $map = $this->getMap(); 42 | if (!isset($map[$name])) { 43 | throw new \Exception("Call to undefined method {$name}"); 44 | } 45 | $this->setAttribute($map[$name], $arguments[0]); 46 | return $this; 47 | } 48 | 49 | public function isDirty() { 50 | return $this->isDirty; 51 | } 52 | 53 | public function markDirty() { 54 | $this->isDirty = true; 55 | } 56 | 57 | public function markClean() { 58 | $this->isDirty = false; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/builder/entity/enumvariant.php: -------------------------------------------------------------------------------- 1 | $item) { 19 | $this->setAttribute($key, $item); 20 | } 21 | if ($data['ID']) { 22 | $this->setId($data['ID']); 23 | } 24 | $this->value($value); 25 | } 26 | 27 | /** 28 | * @param int $id 29 | * @return EnumVariant 30 | */ 31 | public function setId($id) { 32 | $this->id = $id; 33 | return $this; 34 | } 35 | 36 | /** 37 | * @return int 38 | */ 39 | public function getId() { 40 | return $this->id; 41 | } 42 | 43 | protected function getMap() { 44 | return array( 45 | 'value' => 'VALUE', 46 | 'sort' => 'SORT', 47 | ); 48 | } 49 | 50 | /** 51 | * @param string $value 52 | * 53 | * @return EnumVariant 54 | */ 55 | public function xmlId($value) { 56 | $this->setAttribute('XML_ID', $value); 57 | $this->setAttribute('EXTERNAL_ID', $value); 58 | return $this; 59 | } 60 | 61 | 62 | /** 63 | * @param bool $value 64 | * 65 | * @return EnumVariant 66 | */ 67 | public function byDefault($value = true) { 68 | $this->setAttribute('DEF', $value ? 'Y' : 'N'); 69 | return $this; 70 | } 71 | 72 | /** 73 | * @return EnumVariant 74 | */ 75 | public function markDeleted() { 76 | $this->setAttribute('DEL', 'Y'); 77 | return $this; 78 | } 79 | 80 | public function needToDelete() { 81 | return $this->getAttribute('DEL'); 82 | } 83 | 84 | } -------------------------------------------------------------------------------- /lib/builder/entity/eventmessage.php: -------------------------------------------------------------------------------- 1 | $value) { 27 | $this->setAttribute($code, $value); 28 | } 29 | $this 30 | ->emailFrom($from) 31 | ->active() 32 | ->emailTo($to) 33 | ->siteId($siteId) 34 | ->dateUpdate(new DateTime()); 35 | 36 | } 37 | 38 | /** 39 | * @param int $id 40 | * @return EventMessage 41 | */ 42 | public function setId($id) { 43 | $this->id = $id; 44 | return $this; 45 | } 46 | 47 | /** 48 | * @return int 49 | */ 50 | public function getId() { 51 | return $this->id; 52 | } 53 | 54 | protected function getMap() { 55 | return array( 56 | 'id' => 'ID', 57 | 'siteId' => 'LID', 58 | 'active' => 'ACTIVE', 59 | 'emailFrom' => 'EMAIL_FROM', 60 | 'emailTo' => 'EMAIL_TO', 61 | 'subject' => 'SUBJECT', 62 | 'body' => 'MESSAGE', 63 | 'bodyType' => 'BODY_TYPE', 64 | 'bcc' => 'BCC', 65 | 'dateUpdate' => 'TIMESTAMP_X', 66 | ); 67 | } 68 | 69 | /** 70 | * @param bool $active 71 | * @return EventMessage 72 | */ 73 | public function active($active = true) { 74 | $this->setAttribute('ACTIVE', $active ? 'Y' : 'N'); 75 | return $this; 76 | } 77 | 78 | public function remove() { 79 | $this->forRemove = true; 80 | } 81 | 82 | public function isRemoved() { 83 | return $this->forRemove; 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /lib/builder/entity/eventtype.php: -------------------------------------------------------------------------------- 1 | eventName($eventName) 25 | ->lid($lid); 26 | } 27 | 28 | protected function getMap() { 29 | return array( 30 | 'sort' => 'SORT', 31 | 'eventName' => 'EVENT_NAME', 32 | 'lid' => 'LID', 33 | 'name' => 'NAME', 34 | 'description' => 'DESCRIPTION', 35 | ); 36 | } 37 | 38 | /** 39 | * @param int $id 40 | * @return EventType 41 | */ 42 | public function setId($id) { 43 | $this->id = $id; 44 | return $this; 45 | } 46 | 47 | /** 48 | * @return int 49 | */ 50 | public function getId() { 51 | return $this->id; 52 | } 53 | 54 | /** 55 | * @return string 56 | */ 57 | public function getEventName() { 58 | return $this->getAttribute('EVENT_NAME'); 59 | } 60 | 61 | /** 62 | * @param string $from 63 | * @param string $to 64 | * @param string $siteId 65 | * 66 | * @return EventMessage 67 | */ 68 | public function addEventMessage($from, $to, $siteId) { 69 | $eventMessage = new EventMessage($from, $to, $siteId); 70 | $this->eventMessages[] = $eventMessage; 71 | return $eventMessage; 72 | } 73 | 74 | /** 75 | * 76 | * @return EventMessage[] 77 | */ 78 | public function loadEventMessages() { 79 | $messages = $this->findMessages(); 80 | foreach ($messages as $message) { 81 | $eventMessage = new EventMessage($message['EMAIL_FROM'], $message['EMAIL_TO'], $message['LID'], $message); 82 | $eventMessage->setId($message['ID']); 83 | $eventMessage->markClean(); 84 | $this->eventMessages[] = $eventMessage; 85 | } 86 | return $this->eventMessages; 87 | } 88 | 89 | /** 90 | * @return mixed 91 | */ 92 | public function getEventMessages() { 93 | return $this->eventMessages; 94 | } 95 | 96 | /** 97 | * @return array 98 | */ 99 | private function findMessages() { 100 | $res = EventMessageTable::getList(array( 101 | 'filter' => array( 102 | 'EVENT_NAME' => $this->getEventName() 103 | ) 104 | )); 105 | 106 | return $res->fetchAll(); 107 | } 108 | } -------------------------------------------------------------------------------- /lib/builder/entity/fieldwrapper.php: -------------------------------------------------------------------------------- 1 | field = $field; 15 | } 16 | 17 | /** 18 | * @return string 19 | */ 20 | public function getName() { 21 | return strtoupper($this->field->getColumnName()); 22 | } 23 | 24 | /** 25 | * @param bool $increment 26 | * 27 | * @return $this 28 | */ 29 | public function autoincrement($increment = true) { 30 | $this->field->configureAutocomplete($increment); 31 | 32 | return $this; 33 | } 34 | 35 | /** 36 | * @param bool $primary 37 | * 38 | * @return $this 39 | */ 40 | public function primary($primary = true) { 41 | $this->field->configurePrimary($primary); 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * @param bool $unique 48 | * 49 | * @return $this 50 | */ 51 | public function unique($unique = true) { 52 | $this->field->configureUnique($unique); 53 | 54 | return $this; 55 | } 56 | 57 | /** 58 | * @param bool $required 59 | * 60 | * @return $this 61 | */ 62 | public function required($required = true) { 63 | $this->field->configureRequired($required); 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * @return ScalarField 70 | */ 71 | public function getField() { 72 | return $this->field; 73 | } 74 | 75 | /** 76 | * @return bool 77 | */ 78 | public function isAutoincrement() { 79 | return $this->field->isAutocomplete(); 80 | } 81 | 82 | /** 83 | * @return bool 84 | */ 85 | public function isPrimary() { 86 | return $this->field->isPrimary(); 87 | } 88 | 89 | /** 90 | * @return bool 91 | */ 92 | public function isUnique() { 93 | return $this->field->isUnique(); 94 | } 95 | 96 | /** 97 | * @return bool 98 | */ 99 | public function isRequired() { 100 | return $this->field->isRequired(); 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /lib/builder/entity/form.php: -------------------------------------------------------------------------------- 1 | name($name) 38 | ->sid($sid); 39 | } 40 | 41 | public function getMap() { 42 | return array( 43 | 'name' => 'NAME', 44 | 'sid' => 'SID', 45 | 'sort' => 'C_SORT', 46 | 'button' => 'BUTTON', 47 | 'useRestrictions' => 'USE_RESTRICTIONS', 48 | 'useCaptcha' => 'USE_CAPTCHA', 49 | 'restrictUser' => 'RESTRICT_USER', 50 | 'restrictTime' => 'RESTRICT_TIME', 51 | 'description' => 'DESCRIPTION', 52 | 'descriptionType' => 'DESCRIPTION_TYPE', 53 | 'filterResultTemplate' => 'FILTER_RESULT_TEMPLATE', 54 | 'tableResultTemplate' => 'TABLE_RESULT_TEMPLATE', 55 | 'statEvent1' => 'STAT_EVENT1', 56 | 'statEvent2' => 'STAT_EVENT2', 57 | 'statEvent3' => 'STAT_EVENT3', 58 | 'image' => 'arIMAGE', 59 | 'arSiteId' => 'arSITE', 60 | 'arMailTemplate' => 'arMAIL_TEMPLATE', 61 | 'arMenu' => 'arMENU', 62 | 'arGroup' => 'arGROUP', 63 | ); 64 | } 65 | 66 | /** 67 | * @param int $id 68 | * @return Form 69 | */ 70 | public function setId($id) { 71 | $this->id = $id; 72 | return $this; 73 | } 74 | 75 | /** 76 | * @return int 77 | */ 78 | public function getId() { 79 | return $this->id; 80 | } 81 | 82 | /** 83 | * @param bool $useCaptcha 84 | * @return Form 85 | */ 86 | public function useCaptcha($useCaptcha) { 87 | $this->setAttribute('USE_CAPTCHA', $useCaptcha ? 'Y' : 'N'); 88 | return $this; 89 | } 90 | 91 | /** 92 | * @param $title 93 | * @return FormStatus 94 | * @throws BuilderException 95 | */ 96 | public function addStatus($title) { 97 | 98 | $status = new FormStatus($title); 99 | $this->statuses[] = $status; 100 | return $status; 101 | } 102 | 103 | /** 104 | * @param $title 105 | * @return FormStatus 106 | * @throws BuilderException 107 | */ 108 | public function updateStatus($title) { 109 | 110 | $data = $this->findStatus($title); 111 | $status = new FormStatus($title); 112 | $status->setId($data['ID']); 113 | $status->markClean(); 114 | $this->statuses[] = $status; 115 | return $status; 116 | } 117 | 118 | /** 119 | * @param $title 120 | * @return array 121 | * @throws BuilderException 122 | */ 123 | private function findStatus($title) { 124 | $status = \CFormStatus::GetList($this->getId(), $by, $order, array( 125 | 'TITLE' => $title, 126 | ), $isFiltered)->Fetch(); 127 | 128 | if (empty($status)) { 129 | throw new BuilderException("Form status '{$title}' not found"); 130 | } 131 | return $status; 132 | } 133 | 134 | /** 135 | * @param $sid 136 | * @return FormField 137 | * @throws BuilderException 138 | */ 139 | public function addField($sid) { 140 | $field = new FormField($sid); 141 | $this->fields[] = $field; 142 | return $field; 143 | } 144 | 145 | /** 146 | * @param $sid 147 | * @return FormField 148 | * @throws BuilderException 149 | */ 150 | public function updateField($sid) { 151 | $data = $this->findField($sid); 152 | $field = new FormField($sid); 153 | $field->setId($data['ID']); 154 | $field->markClean(); 155 | $this->fields[] = $field; 156 | return $field; 157 | } 158 | 159 | /** 160 | * @param $sid 161 | * @return array 162 | * @throws BuilderException 163 | */ 164 | private function findField($sid) { 165 | $field = \CFormField::GetList($this->getId(), 'ALL', $by, $order, array( 166 | 'SID' => $sid, 167 | ), $isFiltered)->Fetch(); 168 | if (empty($field)) { 169 | throw new BuilderException("Form field '{$sid}' not found"); 170 | } 171 | return $field; 172 | } 173 | 174 | /** 175 | * @return FormStatus[] 176 | */ 177 | public function getStatuses() { 178 | return $this->statuses; 179 | } 180 | 181 | /** 182 | * @return FormField[] 183 | */ 184 | public function getFields() { 185 | return $this->fields; 186 | } 187 | 188 | } -------------------------------------------------------------------------------- /lib/builder/entity/formanswer.php: -------------------------------------------------------------------------------- 1 | message($message); 32 | } 33 | 34 | public function getMap() { 35 | return array( 36 | 'sort' => 'C_SORT', 37 | 'message' => 'MESSAGE', 38 | 'value' => 'VALUE', 39 | 'active' => 'ACTIVE', 40 | 'fieldType' => 'FIELD_TYPE', 41 | 'fieldWidth' => 'FIELD_WIDTH', 42 | 'fieldHeight' => 'FIELD_HEIGHT', 43 | 'fieldParam' => 'FIELD_PARAM', 44 | 'del' => 'DEL', 45 | ); 46 | } 47 | 48 | /** 49 | * @param int $id 50 | * @return FormAnswer 51 | */ 52 | public function setId($id) { 53 | $this->id = $id; 54 | return $this; 55 | } 56 | 57 | /** 58 | * @return int 59 | */ 60 | public function getId() { 61 | return $this->id; 62 | } 63 | 64 | /** 65 | * @param bool $active 66 | * @return FormAnswer 67 | */ 68 | public function active($active) { 69 | $this->setAttribute('ACTIVE', $active ? 'Y' : 'N'); 70 | return $this; 71 | } 72 | 73 | public function needDelete() { 74 | return $this->getAttribute('DEL') == 'Y'; 75 | } 76 | 77 | public function markDelete() { 78 | $this->setAttribute('DEL', 'Y'); 79 | } 80 | 81 | 82 | } -------------------------------------------------------------------------------- /lib/builder/entity/formfield.php: -------------------------------------------------------------------------------- 1 | setAttribute('SID', $sid); 38 | $this->answers = array(); 39 | } 40 | 41 | public function getMap() { 42 | return array( 43 | 'sort' => 'C_SORT', 44 | 'sid' => 'SID', 45 | 'formId' => 'FORM_ID', 46 | 'active' => 'ACTIVE', 47 | 'additional' => 'ADDITIONAL', 48 | 'fieldType' => 'FIELD_TYPE', 49 | 'title' => 'TITLE', 50 | 'titleType' => 'TITLE_TYPE', 51 | 'required' => 'REQUIRED', 52 | 'filterTitle' => 'FILTER_TITLE', 53 | 'inResultsTable' => 'IN_RESULTS_TABLE', 54 | 'inExcelTable' => 'IN_EXCEL_TABLE', 55 | 'resultsTableTitle' => 'RESULTS_TABLE_TITLE', 56 | 'comments' => 'COMMENTS', 57 | 'arImage' => 'arIMAGE', 58 | 'arFilterUser' => 'arFILTER_USER', 59 | 'arFilterAnswerText' => 'arFILTER_ANSWER_TEXT', 60 | 'arFilterAnswerValue' => 'arFILTER_ANSWER_VALUE', 61 | 'arFilterField' => 'arFILTER_FIELD', 62 | ); 63 | } 64 | 65 | /** 66 | * @param int $id 67 | * @return FormField 68 | */ 69 | public function setId($id) { 70 | $this->id = $id; 71 | return $this; 72 | } 73 | 74 | /** 75 | * @return int 76 | */ 77 | public function getId() { 78 | return $this->id; 79 | } 80 | 81 | /** 82 | * @param bool $active 83 | * @return FormField 84 | */ 85 | public function active($active) { 86 | $this->setAttribute('ACTIVE', $active ? 'Y' : 'N'); 87 | return $this; 88 | } 89 | 90 | /** 91 | * @return FormField 92 | */ 93 | public function asQuestion() { 94 | $this->setAttribute('ADDITIONAL', 'N'); 95 | return $this; 96 | } 97 | 98 | /** 99 | * @return FormField 100 | */ 101 | public function asField() { 102 | $this->setAttribute('ADDITIONAL', 'Y'); 103 | return $this; 104 | } 105 | 106 | /** 107 | * @param bool $required 108 | * @return FormField 109 | */ 110 | public function required($required = true) { 111 | $this->setAttribute('REQUIRED', $required ? "Y" : "N"); 112 | return $this; 113 | } 114 | 115 | /** 116 | * @param bool $inResultsTable 117 | * @return FormField 118 | */ 119 | public function inResultsTable($inResultsTable) { 120 | $this->setAttribute('IN_RESULTS_TABLE', $inResultsTable ? "Y" : "N"); 121 | return $this; 122 | } 123 | 124 | /** 125 | * @param bool $inExcelTable 126 | * @return FormField 127 | */ 128 | public function inExcelTable($inExcelTable) { 129 | $this->setAttribute('IN_EXCEL_TABLE', $inExcelTable ? "Y" : "N"); 130 | return $this; 131 | } 132 | 133 | /** 134 | * @param $message 135 | * @return FormAnswer 136 | */ 137 | public function addAnswer($message) { 138 | $answer = new FormAnswer($message); 139 | $this->answers[] = $answer; 140 | return $answer; 141 | } 142 | 143 | /** 144 | * @param $message 145 | * @return FormAnswer 146 | * @throws BuilderException 147 | */ 148 | public function updateAnswer($message) { 149 | $data = $this->findAnswer($message); 150 | $answer = new FormAnswer($message); 151 | $answer->setId($data['ID']); 152 | $answer->markClean(); 153 | $this->answers[] = $answer; 154 | return $answer; 155 | } 156 | 157 | /** 158 | * @param $message 159 | * 160 | * @return FormAnswer 161 | * @throws BuilderException 162 | */ 163 | public function removeAnswer($message) { 164 | $data = $this->findAnswer($message); 165 | $answer = new FormAnswer($message); 166 | $answer->markDelete(); 167 | $answer->setId($data['ID']); 168 | $this->answers[] = $answer; 169 | return $answer; 170 | } 171 | 172 | private function findAnswer($message) { 173 | $data = \CFormAnswer::GetList($this->getId(), $by = null, $order = null, array( 174 | 'MESSAGE' => $message 175 | ), $isFiltered = false)->Fetch(); 176 | 177 | if (empty($data)) { 178 | throw new BuilderException("Answer '{$message}' not found"); 179 | } 180 | return $data; 181 | } 182 | 183 | /** 184 | * @return FormAnswer[] 185 | */ 186 | public function getAnswers() { 187 | return $this->answers; 188 | } 189 | 190 | } -------------------------------------------------------------------------------- /lib/builder/entity/formstatus.php: -------------------------------------------------------------------------------- 1 | title($title) 28 | ->dateUpdate(new DateTime()); 29 | } 30 | 31 | public function getMap() { 32 | return array( 33 | 'sort' => 'C_SORT', 34 | 'dateUpdate' => 'TIMESTAMP_X', 35 | 'active' => 'ACTIVE', 36 | 'title' => 'TITLE', 37 | 'description' => 'DESCRIPTION', 38 | 'isDefault' => 'DEFAULT_VALUE', 39 | 'css' => 'CSS', 40 | 'handlerOut' => 'HANDLER_OUT', 41 | 'handlerIn' => 'HANDLER_IN', 42 | 'arGroupCanView' => 'arPERMISSION_VIEW', 43 | 'arGroupCanMove' => 'arPERMISSION_MOVE', 44 | 'arGroupCanEdit' => 'arPERMISSION_EDIT', 45 | 'arGroupCanDelete' => 'arPERMISSION_DELETE', 46 | ); 47 | } 48 | 49 | /** 50 | * @param int $id 51 | * @return FormStatus 52 | */ 53 | public function setId($id) { 54 | $this->id = $id; 55 | return $this; 56 | } 57 | 58 | /** 59 | * @return int 60 | */ 61 | public function getId() { 62 | return $this->id; 63 | } 64 | 65 | /** 66 | * @param bool $active 67 | * @return FormStatus 68 | */ 69 | public function active($active) { 70 | $this->setAttribute('ACTIVE', $active ? 'Y' : 'N'); 71 | return $this; 72 | } 73 | 74 | /** 75 | * @param bool $isDefault 76 | * @return FormStatus 77 | */ 78 | public function byDefault($isDefault = true) { 79 | $this->setAttribute('DEFAULT_VALUE', $isDefault ? 'Y' : 'N'); 80 | return $this; 81 | } 82 | 83 | } -------------------------------------------------------------------------------- /lib/builder/entity/highloadblock.php: -------------------------------------------------------------------------------- 1 | id = $id; 22 | $this->name($name); 23 | $this->tableName($tableName); 24 | } 25 | 26 | public function getMap() { 27 | return array( 28 | 'name' => 'NAME', 29 | 'tableName' => 'TABLE_NAME', 30 | ); 31 | } 32 | 33 | /** 34 | * @param int $id 35 | * @return HighLoadBlock 36 | */ 37 | public function setId($id) { 38 | $this->id = $id; 39 | return $this; 40 | } 41 | 42 | /** 43 | * @return int 44 | */ 45 | public function getId() { 46 | return $this->id; 47 | } 48 | 49 | /** 50 | * @param $code 51 | * @return UserField 52 | */ 53 | public function addField($code) { 54 | return $this->addUserField($code); 55 | } 56 | 57 | /** 58 | * @param $code 59 | * @return UserField 60 | * @throws BuilderException 61 | */ 62 | public function updateField($code) { 63 | if (!$this->getId()) { 64 | throw new BuilderException('Set higloadBlock for continue'); 65 | } 66 | 67 | return $this->updateUserField($code, "HLBLOCK_{$this->getId()}"); 68 | } 69 | 70 | /** 71 | * @return UserField[] 72 | */ 73 | public function getFields() { 74 | return $this->getUserFields(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /lib/builder/entity/iblocktype.php: -------------------------------------------------------------------------------- 1 | ['NAME'=>'Catalog', 'SECTION_NAME'=>'Sections', 'ELEMENT_NAME'=>'Products']] 12 | * @package WS\ReduceMigrations\Builder\Entity 13 | */ 14 | class IblockType extends Base { 15 | 16 | public function __construct($type) { 17 | $this->setId($type); 18 | $this->type($type); 19 | } 20 | 21 | /** 22 | * @param string $id 23 | * @return IblockType 24 | */ 25 | public function setId($id) { 26 | $this->setAttribute('ID', $id); 27 | return $this; 28 | } 29 | 30 | /** 31 | * @return string 32 | */ 33 | public function getId() { 34 | return $this->getAttribute('ID'); 35 | } 36 | 37 | protected function getMap() { 38 | return array( 39 | 'type' => 'IBLOCK_TYPE_ID', 40 | 'sort' => 'SORT', 41 | 'sections' => 'SECTIONS', 42 | 'lang' => 'LANG', 43 | ); 44 | } 45 | 46 | /** 47 | * @param bool $inRss 48 | * @return IblockType 49 | */ 50 | public function inRss($inRss) { 51 | $this->setAttribute('IN_RSS', $inRss ? 'Y' : 'N'); 52 | return $this; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/builder/entity/table.php: -------------------------------------------------------------------------------- 1 | name = $name; 20 | } 21 | 22 | private function addField($scalarField) { 23 | $field = new FieldWrapper($scalarField); 24 | $this->fields[$field->getName()] = $field; 25 | return $field; 26 | } 27 | 28 | /** 29 | * @param $name 30 | * 31 | * @return FieldWrapper 32 | */ 33 | public function string($name) { 34 | return $this->addField(new StringField($name)); 35 | } 36 | 37 | /** 38 | * @param $name 39 | * 40 | * @return FieldWrapper 41 | */ 42 | public function integer($name) { 43 | return $this->addField(new IntegerField($name)); 44 | } 45 | 46 | /** 47 | * @param $name 48 | * 49 | * @return FieldWrapper 50 | */ 51 | public function float($name) { 52 | return $this->addField(new FloatField($name)); 53 | } 54 | 55 | /** 56 | * @param $name 57 | * 58 | * @return FieldWrapper 59 | */ 60 | public function datetime($name) { 61 | return $this->addField(new DatetimeField($name)); 62 | } 63 | 64 | /** 65 | * @param $name 66 | * 67 | * @return FieldWrapper 68 | */ 69 | public function date($name) { 70 | return $this->addField(new DateField($name)); 71 | } 72 | 73 | /** 74 | * @param $name 75 | * 76 | * @return FieldWrapper 77 | */ 78 | public function text($name) { 79 | return $this->addField(new TextField($name)); 80 | } 81 | 82 | /** 83 | * @param $name 84 | * 85 | * @return FieldWrapper 86 | */ 87 | public function boolean($name) { 88 | return $this->addField(new BooleanField($name)); 89 | } 90 | 91 | /** 92 | * @return mixed 93 | */ 94 | public function getFields() { 95 | return $this->fields; 96 | } 97 | 98 | /** 99 | * @return array 100 | */ 101 | public function getPrimary() { 102 | return array_filter(array_map(function (FieldWrapper $field) { 103 | return $field->isPrimary() ? $field->getName() : false; 104 | }, $this->getFields())); 105 | } 106 | 107 | /** 108 | * @return array 109 | */ 110 | public function getAutoincrement() { 111 | return array_filter(array_map(function (FieldWrapper $field) { 112 | return $field->isAutoincrement() ? $field->getName() : false; 113 | }, $this->getFields())); 114 | } 115 | 116 | /** 117 | * @return array 118 | */ 119 | public function getPreparedFields() { 120 | return array_map(function (FieldWrapper $field) { 121 | return $field->getField(); 122 | }, $this->getFields()); 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /lib/builder/entity/userfield.php: -------------------------------------------------------------------------------- 1 | 'name', 'en' => 'name'] 14 | * @method UserField listLabel(array $value) - ['ru' => 'name', 'en' => 'name'] 15 | * @method UserField filterLabel(array $value) - ['ru' => 'name', 'en' => 'name'] 16 | * @method UserField settings(array $value) 17 | * @method UserField sort(int $value) 18 | * 19 | * @package WS\ReduceMigrations\Builder\Entity 20 | */ 21 | class UserField extends Base { 22 | 23 | const TYPE_VIDEO = 'video'; 24 | const TYPE_HLBLOCK = 'hlblock'; 25 | const TYPE_STRING = 'string'; 26 | const TYPE_INTEGER = 'integer'; 27 | const TYPE_DOUBLE = 'double'; 28 | const TYPE_DATETIME = 'datetime'; 29 | const TYPE_DATE = 'date'; 30 | const TYPE_BOOLEAN = 'boolean'; 31 | const TYPE_FILE = 'file'; 32 | const TYPE_ENUMERATION = 'enumeration'; 33 | const TYPE_IBLOCK_SECTION = 'iblock_section'; 34 | const TYPE_IBLOCK_ELEMENT = 'iblock_element'; 35 | const TYPE_STRING_FORMATTED = 'string_formatted'; 36 | const TYPE_VOTE = 'vote'; 37 | private $enumVariants; 38 | private $id; 39 | 40 | public function __construct($code) { 41 | $this->code(strtoupper($code)); 42 | $this->enumVariants = array(); 43 | } 44 | 45 | public function getMap() { 46 | return array( 47 | 'code' => 'FIELD_NAME', 48 | 'entityId' => 'ENTITY_ID', 49 | 'type' => 'USER_TYPE_ID', 50 | 'xmlId' => 'XML_ID', 51 | 'sort' => 'SORT', 52 | 'multiple' => 'MULTIPLE', 53 | 'required' => 'MANDATORY', 54 | 'showInFilter' => 'SHOW_FILTER', 55 | 'showInList' => 'SHOW_IN_LIST', 56 | 'editInList' => 'EDIT_IN_LIST', 57 | 'searchable' => 'IS_SEARCHABLE', 58 | 'editFormLabel' => 'EDIT_FORM_LABEL', 59 | 'listLabel' => 'LIST_COLUMN_LABEL', 60 | 'filterLabel' => 'LIST_FILTER_LABEL', 61 | 'settings' => 'SETTINGS', 62 | ); 63 | } 64 | 65 | /** 66 | * @param int $id 67 | * @return UserField 68 | */ 69 | public function setId($id) { 70 | $this->id = $id; 71 | return $this; 72 | } 73 | 74 | /** 75 | * @return int 76 | */ 77 | public function getId() { 78 | return $this->id; 79 | } 80 | 81 | /** 82 | * @param $label 83 | * 84 | * @return UserField $this 85 | */ 86 | public function label($label) { 87 | $this->listLabel($label); 88 | $this->editFormLabel($label); 89 | $this->filterLabel($label); 90 | return $this; 91 | } 92 | /** 93 | * @param bool $multiple 94 | * @return UserField 95 | */ 96 | public function multiple($multiple) { 97 | $this->setAttribute('MULTIPLE', $multiple ? 'Y' : 'N'); 98 | return $this; 99 | } 100 | 101 | /** 102 | * @param bool $required 103 | * @return UserField 104 | */ 105 | public function required($required) { 106 | $this->setAttribute('MANDATORY', $required ? 'Y' : 'N'); 107 | return $this; 108 | } 109 | 110 | /** 111 | * @param bool $showInFilter 112 | * @return UserField 113 | */ 114 | public function showInFilter($showInFilter) { 115 | $this->setAttribute('SHOW_FILTER', $showInFilter ? 'Y' : 'N'); 116 | return $this; 117 | } 118 | 119 | /** 120 | * @param bool $showInList 121 | * @return UserField 122 | */ 123 | public function showInList($showInList) { 124 | $this->setAttribute('SHOW_IN_LIST', $showInList ? 'Y' : 'N'); 125 | return $this; 126 | } 127 | 128 | /** 129 | * @param bool $editInList 130 | * @return UserField 131 | */ 132 | public function editInList($editInList) { 133 | $this->setAttribute('EDIT_IN_LIST', $editInList ? 'Y' : 'N'); 134 | return $this; 135 | } 136 | 137 | /** 138 | * @param bool $searchable 139 | * @return UserField 140 | */ 141 | public function searchable($searchable) { 142 | $this->setAttribute('IS_SEARCHABLE', $searchable ? 'Y' : 'N'); 143 | return $this; 144 | } 145 | 146 | /** 147 | * @param $name 148 | * @return EnumVariant 149 | */ 150 | public function addEnum($name) { 151 | $variant = new EnumVariant($name); 152 | $this->enumVariants[] = $variant; 153 | return $variant; 154 | } 155 | 156 | /** 157 | * @param $name 158 | * @return EnumVariant 159 | */ 160 | public function updateEnum($name) { 161 | $data = $this->findEnum($name); 162 | $variant = new EnumVariant($name, $data); 163 | $variant->markClean(); 164 | $this->enumVariants[] = $variant; 165 | return $variant; 166 | } 167 | 168 | /** 169 | * @param $name 170 | * @return UserField 171 | */ 172 | public function removeEnum($name) { 173 | $data = $this->findEnum($name); 174 | $variant = new EnumVariant($name, $data); 175 | $variant->markDeleted(); 176 | $this->enumVariants[] = $variant; 177 | return $this; 178 | } 179 | 180 | /** 181 | * @return EnumVariant[] 182 | */ 183 | public function getEnumVariants() { 184 | return $this->enumVariants; 185 | } 186 | 187 | /** 188 | * @param $name 189 | * @return array 190 | * @throws BuilderException 191 | */ 192 | private function findEnum($name) { 193 | if (!$this->getId()) { 194 | throw new BuilderException('Save Field before update enum'); 195 | } 196 | $res = \CUserFieldEnum::GetList(null, array( 197 | 'USER_FIELD_ID' => $this->getId(), 198 | 'VALUE' => $name, 199 | ))->Fetch(); 200 | if (empty($res)) { 201 | throw new BuilderException("Enum for `$name` not found"); 202 | } 203 | return $res; 204 | } 205 | 206 | } -------------------------------------------------------------------------------- /lib/builder/eventsbuilder.php: -------------------------------------------------------------------------------- 1 | commit($eventType); 21 | return $eventType; 22 | } 23 | 24 | /** 25 | * @param string $type 26 | * @param string $lid 27 | * @param \Closure $callback 28 | * @return EventType 29 | * @throws BuilderException 30 | */ 31 | public function updateEventType($type, $lid, $callback) { 32 | $data = $this->findEventType($type, $lid); 33 | $eventType = new EventType($data['EVENT_NAME'], $data['LID']); 34 | $eventType->setId($data['ID']); 35 | $eventType->markClean(); 36 | $callback($eventType); 37 | $this->commit($eventType); 38 | return $eventType; 39 | } 40 | 41 | /** 42 | * @param EventType $eventType 43 | * 44 | * @throws BuilderException 45 | */ 46 | public function commit($eventType) { 47 | global $DB; 48 | $DB->StartTransaction(); 49 | try { 50 | $this->commitEventType($eventType); 51 | } catch (\Exception $e) { 52 | $DB->Rollback(); 53 | throw new BuilderException($e->getMessage()); 54 | } 55 | $DB->Commit(); 56 | } 57 | 58 | /** 59 | * @param $type 60 | * @param $lid 61 | * @return array 62 | * @throws BuilderException 63 | */ 64 | private function findEventType($type, $lid) { 65 | $data = \CEventType::GetList(array( 66 | 'TYPE_ID' => $type, 67 | 'LID' => $lid 68 | ))->Fetch(); 69 | if (empty($data)) { 70 | throw new BuilderException("EventType '{$type}' not found for lid '{$lid}'"); 71 | } 72 | return $data; 73 | } 74 | 75 | /** 76 | * @param EventType $eventType 77 | * 78 | * @throws BuilderException 79 | */ 80 | private function commitEventType($eventType) { 81 | global $APPLICATION; 82 | 83 | $gw = new \CEventType(); 84 | if ($eventType->getId() > 0) { 85 | if ($eventType->isDirty()) { 86 | $result = $gw->Update(array('ID' => $eventType->getId()), $eventType->getData()); 87 | if (!$result) { 88 | throw new BuilderException('EventType update failed with error: ' . $APPLICATION->GetException()->GetString()); 89 | } 90 | } 91 | 92 | } else { 93 | $result = $gw->Add($eventType->getData()); 94 | if (!$result) { 95 | throw new BuilderException('EventType add failed with error: ' . $APPLICATION->GetException()->GetString()); 96 | } 97 | $eventType->setId($result); 98 | } 99 | $this->commitEventMessages($eventType->getEventName(), $eventType->getEventMessages()); 100 | } 101 | 102 | /** 103 | * @param string $eventName 104 | * @param EventMessage[] $eventMessages 105 | * 106 | * @throws BuilderException 107 | */ 108 | private function commitEventMessages($eventName, $eventMessages) { 109 | global $APPLICATION; 110 | 111 | $gw = new \CEventMessage(); 112 | foreach ($eventMessages as $message) { 113 | if ($message->getId() > 0) { 114 | if ($message->isRemoved() && !$gw->Delete($message->getId())) { 115 | throw new BuilderException("EventType wasn't deleted: ". $APPLICATION->GetException()->GetString()); 116 | } 117 | if ($message->isDirty() && !$gw->Update($message->getId(), $message->getData())) { 118 | throw new BuilderException("EventType wasn't updated: ". $APPLICATION->GetException()->GetString()); 119 | } 120 | } else { 121 | $id = $gw->Add(array_merge( 122 | $message->getData(), 123 | array('EVENT_NAME' => $eventName) 124 | )); 125 | if (!$id) { 126 | throw new BuilderException("EventMessage add failed with error: " . $APPLICATION->GetException()->GetString()); 127 | } 128 | $message->setId($id); 129 | } 130 | 131 | } 132 | } 133 | 134 | } 135 | -------------------------------------------------------------------------------- /lib/builder/formbuilder.php: -------------------------------------------------------------------------------- 1 | commit($form); 27 | return $form; 28 | } 29 | 30 | /** 31 | * @param string $sid 32 | * @param \Closure $callback 33 | * @return Form 34 | * @throws BuilderException 35 | */ 36 | public function updateForm($sid, $callback) { 37 | $formData = $this->findForm($sid); 38 | $form = new Form($formData['NAME'], $sid); 39 | $form->setId($formData['ID']); 40 | $form->markClean(); 41 | $callback($form); 42 | $this->commit($form); 43 | return $form; 44 | } 45 | 46 | /** 47 | * @param string $sid 48 | * @return boolean 49 | */ 50 | public function removeForm($sid) { 51 | $formData = $this->findForm($sid); 52 | if (!$formData['ID']) { 53 | return false; 54 | } 55 | return \CForm::Delete($formData['ID']); 56 | } 57 | 58 | /** 59 | * @param Form $form 60 | * @throws BuilderException 61 | */ 62 | protected function commit($form) { 63 | global $DB; 64 | $DB->StartTransaction(); 65 | try { 66 | $this->commitForm($form); 67 | $this->commitFields($form); 68 | $this->commitStatuses($form); 69 | } catch (\Exception $e) { 70 | $DB->Rollback(); 71 | throw new BuilderException($e->getMessage()); 72 | } 73 | $DB->Commit(); 74 | } 75 | 76 | /** 77 | * @param Form $form 78 | * @throws BuilderException 79 | */ 80 | private function commitForm($form) { 81 | global $strError; 82 | if (!$form->isDirty()) { 83 | return ; 84 | } 85 | $formId = \CForm::Set($form->getData(), $form->getId(), 'N'); 86 | if (!$formId) { 87 | throw new BuilderException("Form wasn't saved. " . $strError); 88 | } 89 | $form->setId($formId); 90 | } 91 | 92 | /** 93 | * @param Form $form 94 | * @throws BuilderException 95 | */ 96 | private function commitFields($form) { 97 | global $strError; 98 | $gw = new \CFormField(); 99 | foreach ($form->getFields() as $field) { 100 | if ($field->isDirty()) { 101 | $field->setAttribute('FORM_ID', $form->getId()); 102 | $saveData = $field->getData(); 103 | $fieldId = $gw->Set($saveData, $field->getId(), 'N', 'Y'); 104 | if (!$fieldId) { 105 | throw new BuilderException("Field '{$field->getAttribute('SID')}' wasn't saved. " . $strError); 106 | } 107 | $field->setId($fieldId); 108 | } 109 | 110 | $this->commitAnswers($field); 111 | } 112 | } 113 | 114 | /** 115 | * @param FormField $field 116 | * @throws BuilderException 117 | */ 118 | private function commitAnswers($field) { 119 | global $strError; 120 | $gw = new \CFormAnswer(); 121 | foreach ($field->getAnswers() as $answer) { 122 | if ($answer->needDelete()) { 123 | if (!$gw->Delete($answer->getId())) { 124 | throw new BuilderException("Can't delete '{$answer->getAttribute('MESSAGE')}'. ". $strError); 125 | } 126 | } 127 | $answer->setAttribute('QUESTION_ID', $field->getId()); 128 | $data = $answer->getData(); 129 | if ($answer->isDirty() && !$gw->Set($data, $answer->getId())) { 130 | throw new BuilderException("Answer wasn't saved. " . $strError); 131 | } 132 | } 133 | } 134 | 135 | /** 136 | * @param Form $form 137 | * @throws BuilderException 138 | */ 139 | private function commitStatuses($form) { 140 | global $strError; 141 | $gw = new \CFormStatus(); 142 | foreach ($form->getStatuses() as $status) { 143 | if (!$form->isDirty()) { 144 | continue; 145 | } 146 | $status->setAttribute('FORM_ID', $form->getId()); 147 | $saveData = $status->getData(); 148 | $statusId = $gw->Set($saveData, $status->getId(), 'N'); 149 | if (!$statusId) { 150 | throw new BuilderException("Field '{$status->getAttribute('TITLE')}' wasn't saved. " . $strError); 151 | } 152 | $status->setId($statusId); 153 | } 154 | } 155 | 156 | /** 157 | * @param $sid 158 | * @return array 159 | * @throws BuilderException 160 | */ 161 | private function findForm($sid) { 162 | $data = \CForm::GetList($by = 'ID', $order = 'ASC', array( 163 | 'SID' => $sid 164 | ), $isFiltered = false)->Fetch(); 165 | 166 | if (!$data) { 167 | throw new BuilderException("Form '{$sid}' not found"); 168 | } 169 | 170 | return $data; 171 | } 172 | 173 | } 174 | -------------------------------------------------------------------------------- /lib/builder/highloadblockbuilder.php: -------------------------------------------------------------------------------- 1 | commit($highLoadBlock); 29 | return $highLoadBlock; 30 | } 31 | 32 | /** 33 | * @param string $tableName 34 | * @param \Closure $callback 35 | * 36 | * @return HighLoadBlock 37 | * @throws BuilderException 38 | */ 39 | public function updateHLBlock($tableName, $callback) { 40 | $block = $this->findTable($tableName); 41 | $highLoadBlock = new HighLoadBlock($block['NAME'], $tableName, $block['ID']); 42 | $highLoadBlock->markClean(); 43 | $callback($highLoadBlock); 44 | $this->commit($highLoadBlock); 45 | return $highLoadBlock; 46 | } 47 | 48 | /** 49 | * @var HighLoadBlock $highLoadBlock 50 | * @throws BuilderException 51 | */ 52 | private function commit($highLoadBlock) { 53 | global $DB; 54 | $DB->StartTransaction(); 55 | try { 56 | $this->commitHighLoadBlock($highLoadBlock); 57 | $this->commitFields($highLoadBlock); 58 | } catch (BuilderException $e) { 59 | $DB->Rollback(); 60 | throw new BuilderException($e->getMessage()); 61 | } 62 | $DB->Commit(); 63 | } 64 | 65 | /** 66 | * @param $tableName 67 | * @return array|false 68 | * @throws BuilderException 69 | * @throws \Bitrix\Main\ArgumentException 70 | */ 71 | public function findTable($tableName) { 72 | $hbRes = HighloadBlockTable::getList(array( 73 | 'filter' => array( 74 | 'TABLE_NAME' => $tableName 75 | ) 76 | )); 77 | if (!($table = $hbRes->fetch())){ 78 | throw new BuilderException('Cant find block by table name `'.$tableName.'` '); 79 | } 80 | return $table; 81 | } 82 | 83 | /** 84 | * @var HighLoadBlock $highLoadBlock 85 | * @throws BuilderException 86 | * @throws \Bitrix\Main\SystemException 87 | */ 88 | private function commitHighLoadBlock($highLoadBlock) { 89 | $isSuccess = true; 90 | if (!$highLoadBlock->getId()) { 91 | $hbRes = HighloadBlockTable::add($highLoadBlock->getData()); 92 | $isSuccess = $hbRes->isSuccess(); 93 | $highLoadBlock->setId($hbRes->getId()); 94 | } elseif ($highLoadBlock->isDirty()) { 95 | $hbRes = HighloadBlockTable::update( 96 | $highLoadBlock->getId(), 97 | $highLoadBlock->getData() 98 | ); 99 | $isSuccess = $hbRes->isSuccess(); 100 | } 101 | if (!$isSuccess) { 102 | throw new BuilderException($highLoadBlock->getAttribute('TABLE_NAME') . ' ' . implode(', ', $hbRes->getErrorMessages())); 103 | } 104 | } 105 | 106 | /** 107 | * @var HighLoadBlock $highLoadBlock 108 | * @throws BuilderException 109 | */ 110 | private function commitFields($highLoadBlock) { 111 | $this->commitUserFields($highLoadBlock->getFields(), "HLBLOCK_{$highLoadBlock->getId()}"); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /lib/builder/iblockpointer.php: -------------------------------------------------------------------------------- 1 | type = $type; 21 | $this->value = $value; 22 | } 23 | 24 | /** 25 | * @param $name 26 | * @return IblockPointer 27 | */ 28 | public static function byName($name) { 29 | return new static(self::TYPE_NAME, $name); 30 | } 31 | 32 | /** 33 | * @param $id 34 | * @return IblockPointer 35 | */ 36 | public static function byId($id) { 37 | return new static(self::TYPE_ID, $id); 38 | } 39 | 40 | /** 41 | * @param $code 42 | * @return IblockPointer 43 | */ 44 | public static function byCode($code) { 45 | return new static(self::TYPE_CODE, $code); 46 | } 47 | 48 | /** 49 | * @return string 50 | */ 51 | public function getType() { 52 | return $this->type; 53 | } 54 | 55 | /** 56 | * @return mixed 57 | */ 58 | public function getValue() { 59 | return $this->value; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lib/builder/tablebuilder.php: -------------------------------------------------------------------------------- 1 | createTable($table); 21 | return $table; 22 | } 23 | 24 | /** 25 | * @param $tableName 26 | */ 27 | public function drop($tableName) { 28 | $database = Application::getConnection(); 29 | if (!$database->isTableExists($tableName)) { 30 | return; 31 | } 32 | $database->dropTable($tableName); 33 | } 34 | 35 | /** 36 | * @param $tableName 37 | * @param $columnName 38 | */ 39 | public function dropColumn($tableName, $columnName) { 40 | $database = Application::getConnection(); 41 | if (!$database->isTableExists($tableName)) { 42 | return; 43 | } 44 | $database->dropColumn($tableName, $columnName); 45 | } 46 | 47 | /** 48 | * @param $tableName 49 | * @param $columnName 50 | * @param $type 51 | */ 52 | public function addColumn($tableName, $columnName, $type) { 53 | $database = Application::getConnection(); 54 | if (!$database->isTableExists($tableName)) { 55 | return; 56 | } 57 | $columnName = strtoupper($columnName); 58 | $type = strtoupper($type); 59 | $sqlHelper = $database->getSqlHelper(); 60 | $database 61 | ->query('ALTER TABLE '. $sqlHelper->quote($tableName).' ADD '.$sqlHelper->quote($columnName) . ' ' . $type); 62 | } 63 | 64 | /** 65 | * @param $tableName 66 | * @param $columnName 67 | * 68 | * @return bool 69 | */ 70 | public function isColumnExists($tableName, $columnName) { 71 | $database = Application::getConnection(); 72 | 73 | $field = $database->getTableField($tableName, $columnName); 74 | 75 | return $field !== null; 76 | } 77 | 78 | /** 79 | * @param Table $table 80 | */ 81 | private function createTable($table) { 82 | $database = Application::getConnection(); 83 | $database 84 | ->createTable($table->name, $table->getPreparedFields(), $table->getPrimary(), $table->getAutoincrement()); 85 | } 86 | 87 | } 88 | -------------------------------------------------------------------------------- /lib/builder/traits/containuserfieldstrait.php: -------------------------------------------------------------------------------- 1 | user_fields[] = $field; 27 | 28 | return $field; 29 | } 30 | 31 | /** 32 | * @param string $code 33 | * @param string $entity_id 34 | * @return UserField 35 | * @throws BuilderException 36 | */ 37 | private function updateUserField($code, $entity_id) { 38 | $data = $this->findUserField($code, $entity_id); 39 | $field = new UserField($code); 40 | $field->setId($data['ID']); 41 | $field->markClean(); 42 | $this->user_fields[] = $field; 43 | 44 | return $field; 45 | } 46 | 47 | /** 48 | * @param string $code 49 | * @param string $entity_id 50 | * @return array 51 | * @throws BuilderException 52 | */ 53 | private function findUserField($code, $entity_id) { 54 | $field = \CUserTypeEntity::GetList(null, array( 55 | 'FIELD_NAME' => $code, 56 | 'ENTITY_ID' => $entity_id, 57 | ))->Fetch(); 58 | 59 | if (empty($field)) { 60 | throw new BuilderException("Field for `$code` not found"); 61 | } 62 | 63 | return $field; 64 | } 65 | 66 | /** 67 | * @return UserField[] 68 | */ 69 | private function getUserFields() { 70 | return $this->user_fields; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /lib/builder/traits/operateuserfieldentitytrait.php: -------------------------------------------------------------------------------- 1 | getId() > 0) { 29 | $field->isDirty() && $res = $gw->Update($field->getId(), $field->getData()); 30 | } else { 31 | $res = $gw->Add(array_merge($field->getData(), array( 32 | 'ENTITY_ID' => $entity_id, 33 | ))); 34 | if ($res) { 35 | $field->setId($res); 36 | } 37 | } 38 | if (!$res) { 39 | throw new BuilderException($APPLICATION->GetException()->GetString()); 40 | } 41 | 42 | $this->commitUserFieldEnum($field); 43 | } 44 | } 45 | 46 | /** 47 | * @param UserField $field 48 | * @throws BuilderException 49 | */ 50 | private function commitUserFieldEnum($field) { 51 | global $APPLICATION; 52 | $obEnum = new \CUserFieldEnum; 53 | $values = array(); 54 | foreach ($field->getEnumVariants() as $key => $variant) { 55 | $key = 'n' . $key; 56 | if ($variant->getId() > 0) { 57 | $key = $variant->getId(); 58 | } 59 | $values[$key] = $variant->getData(); 60 | } 61 | if (empty($values)) { 62 | return; 63 | } 64 | if (!$obEnum->SetEnumValues($field->getId(), $values)) { 65 | throw new BuilderException($APPLICATION->GetException()->GetString()); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/collection/migrationcollection.php: -------------------------------------------------------------------------------- 1 | getName()); 14 | if (!class_exists($fileClass)) { 15 | include $file->getPath(); 16 | } 17 | 18 | if (!is_subclass_of($fileClass, '\WS\ReduceMigrations\Scenario\ScriptScenario')) { 19 | continue; 20 | } 21 | if (!$fileClass::isValid()) { 22 | continue; 23 | } 24 | $this->elements[] = $fileClass; 25 | } 26 | } 27 | 28 | /** 29 | * @return float|int 30 | */ 31 | public function getApproximateTime() { 32 | $time = 0; 33 | foreach ($this->elements as $element) { 34 | $time += (double)$element::approximatelyTime(); 35 | } 36 | return $time; 37 | } 38 | 39 | /** 40 | * @return array 41 | */ 42 | public function groupByPriority() { 43 | $elements = array(); 44 | $priorities = ScriptScenario::getPriorities(); 45 | foreach ($priorities as $priority) { 46 | $elements[$priority] = array(); 47 | } 48 | 49 | foreach ($this->elements as $key => $element) { 50 | $elements[$element::priority()][] = $element; 51 | } 52 | 53 | return array_filter($elements); 54 | } 55 | 56 | /** 57 | * @return ScriptScenario[] 58 | */ 59 | public function toArray() { 60 | $migrations = $this->groupByPriority(); 61 | $result = array(); 62 | array_walk_recursive($migrations, function($item) use (& $result) { 63 | $result[] = $item; 64 | }); 65 | return $result; 66 | } 67 | 68 | /** 69 | * @return int 70 | */ 71 | public function count() { 72 | return count($this->elements); 73 | } 74 | 75 | /** 76 | * @param $migrationHash 77 | * 78 | * @return ScriptScenario[] 79 | */ 80 | public function findByHash($migrationHash) { 81 | $list = array(); 82 | foreach ($this->elements as $element) { 83 | if (strpos($element::hash(), $migrationHash) !== 0) { 84 | continue; 85 | } 86 | $list[] = $element; 87 | } 88 | return $list; 89 | } 90 | 91 | 92 | } 93 | -------------------------------------------------------------------------------- /lib/console/command/applycommand.php: -------------------------------------------------------------------------------- 1 | force = isset($params[self::FLAG_FORCE]) ? $params[self::FLAG_FORCE] : false; 22 | $this->skipOptional = isset($params[self::FLAG_SKIP_OPTIONAL]) ? $params[self::FLAG_SKIP_OPTIONAL] : false; 23 | $this->migrationHash = isset($params[0]) ? $params[0] : null; 24 | 25 | } 26 | 27 | public function execute($callback = false) { 28 | $listCommand = new ListCommand($this->console, array($this->migrationHash)); 29 | $listCommand->execute(); 30 | 31 | $notAppliedScenarios = $this->module->getNotAppliedScenarios(); 32 | $count = $notAppliedScenarios->count(); 33 | 34 | if ($count == 0) { 35 | return; 36 | } 37 | if ($this->migrationHash) { 38 | $hasByHash = false; 39 | foreach ($notAppliedScenarios->toArray() as $notAppliedScenario) { 40 | if (strpos($notAppliedScenario::getShortenedHash(), $this->migrationHash) !== false) { 41 | $hasByHash = true; 42 | break; 43 | } 44 | } 45 | if (!$hasByHash) { 46 | return; 47 | } 48 | } 49 | 50 | $this->confirmAction(); 51 | 52 | $this->console 53 | ->printLine("\nApplying new migrations started...\n", Console::OUTPUT_PROGRESS); 54 | 55 | $timer = new Timer(); 56 | $timer->start(); 57 | try { 58 | if ($this->migrationHash) { 59 | $count = 1; 60 | $this->module 61 | ->applyMigrationByHash($this->migrationHash, $callback); 62 | } else { 63 | $count = (int)$this->module 64 | ->applyMigrations($this->skipOptional, $callback); 65 | } 66 | } catch (\Exception $e) { 67 | throw new ConsoleException($e->getMessage()); 68 | } 69 | 70 | $timer->stop(); 71 | $time = $this->console->formatTime($timer->getTime()); 72 | $this->console 73 | ->printLine("Apply action finished! $count items, time {$time}", Console::OUTPUT_PROGRESS); 74 | } 75 | 76 | private function confirmAction() { 77 | if ($this->force) { 78 | return true; 79 | } 80 | 81 | $this->console 82 | ->printLine('Are you sure? (yes|no):'); 83 | 84 | $answer = $this->console->readLine(); 85 | 86 | if ($answer !== self::CONFIRM_WORD) { 87 | exit(); 88 | } 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /lib/console/command/basecommand.php: -------------------------------------------------------------------------------- 1 | console = $console; 17 | $this->initParams($params); 18 | $this->module = Module::getInstance(); 19 | } 20 | 21 | /** 22 | * @return string 23 | */ 24 | static public function className() { 25 | return get_called_class(); 26 | } 27 | protected function initParams($params) {} 28 | 29 | abstract public function execute($callback = false); 30 | 31 | } 32 | -------------------------------------------------------------------------------- /lib/console/command/createscenariocommand.php: -------------------------------------------------------------------------------- 1 | ScriptScenario::PRIORITY_HIGH, 25 | self::MEDIUM_PRIORITY_SHORTCUT => ScriptScenario::PRIORITY_MEDIUM, 26 | self::OPTIONAL_PRIORITY_SHORTCUT => ScriptScenario::PRIORITY_OPTIONAL, 27 | ); 28 | } 29 | 30 | protected function initParams($params) { 31 | $this->name = isset($params[self::PARAM_NAME]) ? trim($params[self::PARAM_NAME]) : false; 32 | $this->priority = isset($params[self::PARAM_PRIORITY]) ? trim($params[self::PARAM_PRIORITY]) : false; 33 | $this->time = isset($params[self::PARAM_TIME]) ? trim($params[self::PARAM_TIME]) : false; 34 | } 35 | 36 | private function getName() { 37 | if ($this->name) { 38 | return $this->name; 39 | } 40 | $this->console 41 | ->printLine('Enter name:'); 42 | $name = $this->console 43 | ->readLine(); 44 | while (!strlen(trim($name))) { 45 | $this->console 46 | ->printLine("Name mustn't be empty. Enter name:"); 47 | $name = $this->console 48 | ->readLine(); 49 | } 50 | return $name; 51 | } 52 | 53 | private function getPriority() { 54 | $priority = $this->normalizePriority($this->priority); 55 | while (!$priority) { 56 | $this->console 57 | ->printLine('Enter priority(h - high, m - medium, o - optional):'); 58 | $priority = $this->normalizePriority($this->console 59 | ->readLine()); 60 | } 61 | return $priority; 62 | } 63 | 64 | private function normalizePriority($priority) { 65 | $priorities = $this->availablePriorities(); 66 | if ($priorities[$priority]) { 67 | return $priorities[$priority]; 68 | } 69 | if (in_array($priority, $priorities, true)) { 70 | return $priority; 71 | } 72 | 73 | return false; 74 | } 75 | 76 | public function execute($callback = false) { 77 | try { 78 | $fileName = $this->module->createScenario($this->prepareName($this->getName()), $this->getPriority(), (int)$this->time); 79 | } catch (\Exception $e) { 80 | $this->console->printLine('An error occurred saving file', Console::OUTPUT_ERROR); 81 | $this->console->printLine($e->getMessage()); 82 | return; 83 | } 84 | $this->console->printLine($fileName, Console::OUTPUT_SUCCESS); 85 | } 86 | 87 | public function prepareName($name) { 88 | /* @var \CMain */ 89 | global $APPLICATION; 90 | $name = $APPLICATION->ConvertCharset($name, mb_detect_encoding($name), LANG_CHARSET); 91 | return $name; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /lib/console/command/history.php: -------------------------------------------------------------------------------- 1 | count = isset($params[0]) ? $params[0] : false; 15 | } 16 | 17 | public function execute($callback = false) { 18 | $lastSetupLog = \WS\ReduceMigrations\Module::getInstance()->getLastSetupLog(); 19 | 20 | if (!$lastSetupLog) { 21 | throw new ConsoleException('Nothing to show'); 22 | } 23 | 24 | $table = new ConsoleTable(); 25 | $table->setCharset(LANG_CHARSET); 26 | 27 | $table->setHeaders(array( 28 | 'Date', 'Name', 'Hash', 'Duration' 29 | )); 30 | $table->setCellsLength(array( 31 | 19, 80, 10, 10 32 | )); 33 | 34 | if (!$this->count) { 35 | $logs = $lastSetupLog->getAppliedLogs(); 36 | } else { 37 | $logs = AppliedChangesLogModel::find(array( 38 | 'order' => array('id' => 'desc'), 39 | 'limit' => $this->count 40 | )); 41 | } 42 | 43 | $count = 0; 44 | $commonDuration = 0; 45 | $setupPaddings = $this->getVerticalPaddingsForSetups($logs); 46 | $currentSetupId = 0; 47 | /** @var AppliedChangesLogModel $log */ 48 | foreach ($logs as $log) { 49 | if ($currentSetupId == 0) { 50 | $currentSetupId = $log->setupLogId; 51 | } 52 | if ($currentSetupId != $log->setupLogId) { 53 | $currentSetupId = $log->setupLogId; 54 | $table->addRow(); 55 | } 56 | $date = ''; 57 | if ($setupPaddings[$log->setupLogId] == 0) { 58 | $date = $log->getDate()->format('d.m.Y H:i:s'); 59 | } 60 | $setupPaddings[$log->setupLogId]--; 61 | 62 | $duration = $this->console->formatTime($log->getTime()); 63 | $log->isFailed() && $duration = "failed"; 64 | $log->isSkipped() && $duration = "skipped"; 65 | 66 | $table->addRow(array( 67 | $date, $log->getName(), $log->getHash(), $duration 68 | )); 69 | if ($log->isFailed()) { 70 | $table->addRow(array('', 'Error: '.$log->getErrorMessage(), '', '')); 71 | } 72 | $count++; 73 | $commonDuration += $log->getTime(); 74 | } 75 | $table->addRow(array( 76 | '-------------------', '----------------------------------------', '----------', '---------' 77 | )); 78 | $table->addRow(array( 79 | '', 'Total: '.$count, '', $this->console->formatTime($commonDuration) 80 | )); 81 | 82 | $this->console->printLine("{$count} Last applied migrations:"); 83 | $this->console->printLine($table->getTable()); 84 | } 85 | 86 | 87 | private function getVerticalPaddingsForSetups($logs) { 88 | $arTrackSetup = array(); 89 | foreach ($logs as $log) { 90 | $arTrackSetup[$log->setupLogId]++; 91 | } 92 | foreach ($arTrackSetup as & $countRecords) { 93 | $countRecords = (int) (($countRecords - 1) / 2); 94 | } 95 | return $arTrackSetup; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /lib/console/command/listcommand.php: -------------------------------------------------------------------------------- 1 | registeredFixes = array(); 23 | $this->localization = Module::getInstance()->getLocalization('admin')->fork('cli'); 24 | $this->hash = $params[0]; 25 | } 26 | 27 | public function execute($callback = false) { 28 | $has = false; 29 | $notAppliedScenarios = $this->module->getNotAppliedScenarios(); 30 | foreach ($notAppliedScenarios->groupByPriority() as $priority => $list) { 31 | /** @var ScriptScenario $notAppliedScenario */ 32 | foreach ($list as $notAppliedScenario) { 33 | if ($this->hash && strpos($notAppliedScenario::hash(), $this->hash) === false) { 34 | continue; 35 | } 36 | 37 | $this->registerFix($priority, $notAppliedScenario); 38 | $has = true; 39 | } 40 | } 41 | !$has && $this->console->printLine("Nothing to apply\n", Console::OUTPUT_SUCCESS); 42 | $has && $this->printRegisteredFixes($notAppliedScenarios->getApproximateTime()); 43 | } 44 | 45 | /** 46 | * @param $priority 47 | * @param ScriptScenario $notAppliedScenario 48 | */ 49 | private function registerFix($priority, $notAppliedScenario) { 50 | $this->registeredFixes[$priority][] = array( 51 | 'name' => $notAppliedScenario::name(), 52 | 'hash' => $notAppliedScenario::getShortenedHash(), 53 | 'time' => $this->console->formatTime($notAppliedScenario::approximatelyTime()), 54 | ); 55 | } 56 | 57 | private function printRegisteredFixes($time) { 58 | $table = new ConsoleTable(); 59 | $table->setCharset(LANG_CHARSET); 60 | 61 | $table->setHeaders(array( 62 | 'Priority', 'Name', 'Hash', '~Duration' 63 | )); 64 | 65 | $table->setCellsLength(array(10, 80, 10, 10)); 66 | 67 | $count = 0; 68 | foreach ($this->registeredFixes as $priority => $fixList) { 69 | $priorityPos = (int) ((count($fixList) - 1) / 2); 70 | 71 | $fixList = array_values($fixList); 72 | foreach ($fixList as $k => $fix) { 73 | $table->addRow(array( 74 | $k == $priorityPos ? $priority : '', $fix['name'], $fix['hash'], $fix['time'] 75 | )); 76 | $count++; 77 | } 78 | $table->addRow(array()); 79 | } 80 | $table->addRow(array( 81 | '----------', '---------------------', '----------', '----------' 82 | )); 83 | $table->addRow(array( 84 | '', 'Total: '.$count, '', $this->console->formatTime($time) 85 | )); 86 | $this->console 87 | ->printLine('List of migrations:') 88 | ->printLine($table->getTable()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /lib/console/command/rollbackcommand.php: -------------------------------------------------------------------------------- 1 | timer = new Timer(); 30 | } 31 | 32 | protected function initParams($params) { 33 | $this->migrationHash = isset($params[0]) ? $params[0] : null; 34 | $this->count = isset($params[self::PARAM_COUNT]) ? (int)$params[self::PARAM_COUNT] : null; 35 | if ($this->count && $this->count < 0) { 36 | $this->count = 0; 37 | } 38 | $this->toHash = isset($params[self::PARAM_TO_HASH]) ? $params[self::PARAM_TO_HASH] : null; 39 | $this->type = $this->identifyType(); 40 | } 41 | 42 | private function identifyType() { 43 | if ($this->migrationHash) { 44 | return self::TYPE_HASH; 45 | } 46 | if ($this->count) { 47 | return self::TYPE_COUNT; 48 | } 49 | if ($this->toHash) { 50 | return self::TYPE_TO_HASH; 51 | } 52 | 53 | return self::TYPE_LAST_BATCH; 54 | } 55 | 56 | public function execute($callback = false) { 57 | 58 | try { 59 | $this->rollback($callback); 60 | } catch (\Exception $e) { 61 | throw new ConsoleException($e->getMessage()); 62 | } 63 | 64 | $this->timer->stop(); 65 | $time = $this->console->formatTime($this->timer->getTime()); 66 | $this->console 67 | ->printLine("Rollback action finished! Time $time", Console::OUTPUT_PROGRESS); 68 | } 69 | 70 | private function rollback($callback = false) { 71 | switch ($this->type) { 72 | case self::TYPE_HASH: 73 | $this->showBatch(AppliedChangesLogModel::findByHash($this->migrationHash)); 74 | $this->confirm("Rollback migration with hash={$this->migrationHash}."); 75 | $this->timer->start(); 76 | $this->module->rollbackByHash($this->migrationHash); 77 | break; 78 | case self::TYPE_COUNT: 79 | $this->showBatch(AppliedChangesLogModel::findLastFewMigrations($this->count)); 80 | $this->confirm("Rollback last {$this->count} migrations."); 81 | $this->timer->start(); 82 | $this->module->rollbackLastFewMigrations($this->count, $callback); 83 | break; 84 | case self::TYPE_TO_HASH: 85 | $this->showBatch(AppliedChangesLogModel::findToHash($this->toHash)); 86 | $this->confirm("Rollback migrations to hash={$this->toHash}."); 87 | $this->timer->start(); 88 | $this->module->rollbackToHash($this->toHash, $callback); 89 | break; 90 | case self::TYPE_LAST_BATCH: 91 | $this->showBatch(AppliedChangesLogModel::findLastBatch()); 92 | $this->confirm('Rollback last batch.'); 93 | $this->timer->start(); 94 | $this->module->rollbackLastBatch($callback); 95 | break; 96 | } 97 | 98 | } 99 | 100 | private function confirm($message) { 101 | $this->console 102 | ->printLine($message . ' Are you sure? (yes|no):'); 103 | 104 | $answer = $this->console 105 | ->readLine(); 106 | 107 | if ($answer !== self::CONFIRM_WORD) { 108 | throw new ConsoleException('Operation cancelled'); 109 | } 110 | 111 | $this->console 112 | ->printLine('Rollback action started...', Console::OUTPUT_PROGRESS); 113 | } 114 | 115 | /** 116 | * @param AppliedChangesLogModel[] $logs 117 | */ 118 | private function showBatch($logs) { 119 | if (empty($logs)) { 120 | return; 121 | } 122 | $table = new ConsoleTable(); 123 | $table->setCharset(LANG_CHARSET); 124 | 125 | $table->setHeaders(array( 126 | 'Date', 'Name', 'Hash', 'Status' 127 | )); 128 | $table->setCellsLength(array( 129 | 19, 80, 10, 10 130 | )); 131 | foreach ($logs as $log) { 132 | $status = 'successful'; 133 | if ($log->isSkipped()) { 134 | $status = 'skipped'; 135 | } elseif ($log->isFailed()) { 136 | $status = 'failed'; 137 | } 138 | $table->addRow(array( 139 | $log->getDate()->format('d.m.Y H:i:s'), $log->getName(), $log->getHash(), $status 140 | )); 141 | } 142 | $table->addRow(array( 143 | '-------------------', '---------------------', '----------', '----------' 144 | )); 145 | $table->addRow(array( 146 | '', 'Total: '.count($logs) 147 | )); 148 | $this->console 149 | ->printLine('Migrations for rollback:') 150 | ->printLine($table->getTable()); 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /lib/console/console.php: -------------------------------------------------------------------------------- 1 | ConvertCharsetArray($args, "UTF-8", LANG_CHARSET); 40 | $this->out = fopen('php://stdout', 'w'); 41 | array_shift($args); 42 | $this->params = $args; 43 | $this->action = isset($this->params[0]) ? $this->params[0] : '--help'; 44 | foreach ($args as $arg) { 45 | if ($arg == '--help') { 46 | $this->action = '--help'; 47 | $index = array_search($this->action, $this->params); 48 | if ($index !== false) { 49 | unset($this->params[$index]); 50 | } 51 | array_unshift($this->params, $this->action); 52 | } 53 | } 54 | $this->successOutput = new Output('green'); 55 | $this->errorOutput = new Output('red'); 56 | $this->progressOutput = new Output('yellow'); 57 | $this->defaultOutput = new Output(); 58 | $this->timeFormatter = new TimeFormatter(array( 59 | 'minutes' => 'min', 60 | 'seconds' => 'sec' 61 | )); 62 | 63 | Module::getInstance()->setScenariosMessageOutput($this); 64 | } 65 | 66 | /** 67 | * @param $str 68 | * @param $type 69 | * @return Console 70 | */ 71 | public function printLine($str, $type = false) { 72 | global $APPLICATION; 73 | $str = $APPLICATION->ConvertCharset($str, LANG_CHARSET, "UTF-8"); 74 | $str = $this->colorize($str, $type); 75 | fwrite($this->out, $str . "\n"); 76 | return $this; 77 | } 78 | 79 | public function println($str) { 80 | return $this->printInProgress($str); 81 | } 82 | 83 | /** 84 | * @param $str 85 | * @return Console 86 | */ 87 | public function printError($str) { 88 | return $this->printLine($str, self::OUTPUT_ERROR); 89 | } 90 | 91 | /** 92 | * @param $str 93 | * @return Console 94 | */ 95 | public function printInProgress($str) { 96 | return $this->printLine($str, self::OUTPUT_PROGRESS); 97 | } 98 | 99 | /** 100 | * @param $str 101 | * @return Console 102 | */ 103 | public function printSuccess($str) { 104 | return $this->printLine($str, self::OUTPUT_SUCCESS); 105 | } 106 | 107 | public function colorize($str, $type = false) { 108 | if ($type) { 109 | $str = $this->getOutput($type)->colorize($str); 110 | } 111 | return $str; 112 | } 113 | 114 | public function readLine() { 115 | return trim(fgets(STDIN)); 116 | } 117 | 118 | /** 119 | * @return BaseCommand 120 | * @throws ConsoleException 121 | */ 122 | public function getCommand() { 123 | $commands = array( 124 | '--help' => HelpCommand::className(), 125 | 'list' => ListCommand::className(), 126 | 'apply' => ApplyCommand::className(), 127 | 'rollback' => RollbackCommand::className(), 128 | 'history' => History::className(), 129 | 'createScenario' => CreateScenarioCommand::className(), 130 | 'create' => CreateScenarioCommand::className(), 131 | ); 132 | if (!$commands[$this->action]) { 133 | throw new ConsoleException("Action `{$this->action}` is not supported"); 134 | } 135 | $params = $this->prepareParams($this->params); 136 | return new $commands[$this->action]($this, $params); 137 | } 138 | 139 | /** 140 | * @param $type 141 | * @return Output 142 | */ 143 | private function getOutput($type) { 144 | switch ($type) { 145 | case 'success': 146 | return $this->successOutput; 147 | break; 148 | case 'error': 149 | return $this->errorOutput; 150 | break; 151 | case 'progress': 152 | return $this->progressOutput; 153 | break; 154 | default:; 155 | } 156 | return $this->defaultOutput; 157 | } 158 | 159 | /** 160 | * @param $params 161 | * 162 | * @return array 163 | */ 164 | private function prepareParams($params) { 165 | array_shift($params); 166 | $namedParams = array(); 167 | $positionalParams = array(); 168 | foreach ($params as $param) { 169 | if (strpos($param, '-') === 0) { 170 | $param = explode('=', $param); 171 | $namedParams[$param[0]] = $param[1] ?: true; 172 | } else { 173 | $positionalParams[] = $param; 174 | } 175 | } 176 | $params = array_merge($positionalParams, $namedParams); 177 | 178 | return $params; 179 | } 180 | 181 | public function formatTime($time) { 182 | return $this->timeFormatter->format($time); 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /lib/console/consoleexception.php: -------------------------------------------------------------------------------- 1 | textColors(); 12 | $this->color = isset($colors[$color]) ? $colors[$color] : $colors['default']; 13 | } 14 | 15 | public function textColors () { 16 | return array( 17 | 'black' => 30, 18 | 'red' => 31, 19 | 'green' => 32, 20 | 'yellow' => 33, 21 | 'blue' => 34, 22 | 'magenta' => 35, 23 | 'cyan' => 36, 24 | 'white' => 37, 25 | 'default' => 0 26 | ); 27 | } 28 | 29 | public function colorize($text) { 30 | return chr(27) . "[{$this->color}m" . $text . chr(27) . "[0m"; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /lib/console/formatter/table.php: -------------------------------------------------------------------------------- 1 | title = $title; 22 | $this->console = $console; 23 | } 24 | 25 | /** 26 | * @param $row 27 | * @param $color 28 | */ 29 | public function addColorRow($row, $color) { 30 | $currentRow = array(); 31 | foreach ($row as $item) { 32 | $currentRow[] = $this->console->colorize($item, $color); 33 | } 34 | $this->rows[] = $currentRow; 35 | } 36 | 37 | public function addRow() { 38 | $this->rows[] = func_get_args(); 39 | } 40 | 41 | public function __toString() { 42 | $result = ''; 43 | $result .= $this->title . "\n"; 44 | $maxLen = array(); 45 | foreach ($this->rows as $row) { 46 | foreach ($row as $index => $value) { 47 | if (!$maxLen[$index]) { 48 | $maxLen[$index] = iconv_strlen($value); 49 | } 50 | if ($maxLen[$index] < iconv_strlen($value)) { 51 | $maxLen[$index] = iconv_strlen($value); 52 | } 53 | } 54 | } 55 | 56 | foreach ($this->rows as $row) { 57 | foreach ($row as $index => $value) { 58 | $result .= mb_str_pad($value, $maxLen[$index] + 3); 59 | } 60 | $result .= "\n"; 61 | } 62 | 63 | return $result; 64 | } 65 | } 66 | 67 | function mb_str_pad($input, $pad_length, $pad_string=' ', $pad_type=STR_PAD_RIGHT) { 68 | if (function_exists('mb_strlen')) { 69 | $diff = strlen($input) - mb_strlen($input); 70 | return str_pad($input, $pad_length + $diff, $pad_string, $pad_type); 71 | } 72 | return str_pad($input, $pad_length, $pad_string, $pad_type); 73 | } -------------------------------------------------------------------------------- /lib/console/runtimecounter.php: -------------------------------------------------------------------------------- 1 | start = microtime(true); 11 | $this->migrationCount = 0; 12 | $this->migrationNumbe = 0; 13 | } 14 | } -------------------------------------------------------------------------------- /lib/console/runtimefixcounter.php: -------------------------------------------------------------------------------- 1 | time = 0; 17 | $this->activeFixName = ''; 18 | $this->fixNames = array(); 19 | $this->fixNumber = 0; 20 | $this->migrationCount = 0; 21 | } 22 | 23 | /** 24 | * @param AppliedChangesLogModel[] $list 25 | */ 26 | public function setFixNamesByLogs($list) { 27 | foreach ($list as $log) { 28 | if ($log->isFailed() || $log->isSkipped()) { 29 | continue; 30 | } 31 | $this->migrationCount++; 32 | continue; 33 | } 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /lib/dumbmessageoutput.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\ReduceMigrations\Entities; 7 | 8 | use Bitrix\Main\Entity\DataManager; 9 | 10 | class AppliedChangesLogTable extends DataManager { 11 | public static function className() { 12 | return get_called_class(); 13 | } 14 | 15 | public static function getFilePath() { 16 | return __FILE__; 17 | } 18 | 19 | public static function getTableName() { 20 | return 'ws_reducemigrations_apply_changes_log'; 21 | } 22 | 23 | public static function getMap() { 24 | return array( 25 | 'ID' => array( 26 | 'data_type' => 'integer', 27 | 'primary' => true, 28 | 'autocomplete' => true 29 | ), 30 | 'SETUP_LOG_ID' => array( 31 | 'data_type' => 'integer' 32 | ), 33 | 'GROUP_LABEL' => array( 34 | 'data_type' => 'string', 35 | 'required' => true, 36 | ), 37 | 'DATE' => array( 38 | 'data_type' => 'datetime', 39 | 'required' => true, 40 | ), 41 | 'SUBJECT' => array( 42 | 'data_type' => 'string', 43 | 'required' => true, 44 | ), 45 | 'UPDATE_DATA' => array( 46 | 'data_type' => 'string', 47 | 'required' => true, 48 | ), 49 | 'STATUS' => array( 50 | 'data_type' => 'integer', 51 | ), 52 | 'DESCRIPTION' => array( 53 | 'data_type' => 'string', 54 | 'required' => true, 55 | ), 56 | 'HASH' => array( 57 | 'data_type' => 'string' 58 | ), 59 | ); 60 | } 61 | } -------------------------------------------------------------------------------- /lib/entities/baseentity.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\ReduceMigrations\Entities; 7 | 8 | 9 | use Bitrix\Main\NotImplementedException; 10 | 11 | abstract class BaseEntity { 12 | public $id; 13 | 14 | private $isNew = true; 15 | 16 | private $_errors = array(); 17 | 18 | static private $_oneRequestsCache = array(); 19 | /** 20 | * @param $props 21 | * @return $this 22 | */ 23 | static public function create($props) { 24 | /** @var $model BaseEntity */ 25 | $model = new static; 26 | foreach ($props as $name => $value) { 27 | $model->{$name} = $value; 28 | } 29 | $model->isNew = false; 30 | return $model; 31 | } 32 | 33 | /** 34 | * @param $fields 35 | * @return $this 36 | */ 37 | static private function createByRow($fields) { 38 | $props = array(); 39 | $fieldsToProps = array_flip(static::map()); 40 | foreach ($fields as $name => $value) { 41 | if (!isset($fieldsToProps[$name])) { 42 | continue; 43 | } 44 | $name = $fieldsToProps[$name]; 45 | $props[$name] = $value; 46 | } 47 | $props = static::modifyFromDb($props); 48 | return self::create($props); 49 | } 50 | 51 | private function getRawFields() { 52 | $result = array(); 53 | $data = array(); 54 | foreach (static::map() as $property => $field) { 55 | $data[$property] = $this->{$property}; 56 | } 57 | $data = static::modifyToDb($data); 58 | foreach (static::map() as $property => $field) { 59 | $result[$field] = $data[$property]; 60 | } 61 | 62 | return $result; 63 | } 64 | 65 | /** 66 | * @param array $params 67 | * @return AppliedChangesLogModel[] 68 | */ 69 | static public function find($params = array()) { 70 | $modelToDb = static::map(); 71 | $fReplaceList = function ($list) use ($modelToDb) { 72 | return array_map(function ($item) use ($modelToDb) { 73 | return $modelToDb[$item]; 74 | }, $list); 75 | }; 76 | 77 | if ($params['select']) { 78 | $params['select'] = $fReplaceList($params['select']); 79 | } 80 | if ($params['group']) { 81 | $pGroup = array(); 82 | foreach ($params['group'] as $field => $value) { 83 | $pGroup[$modelToDb[$field]] = $value; 84 | } 85 | $params['group'] = $pGroup; 86 | } 87 | if ($params['order']) { 88 | $pOrder = array(); 89 | foreach ($params['order'] as $field => $value) { 90 | $pOrder[$modelToDb[$field]] = $value; 91 | } 92 | $params['order'] = $pOrder; 93 | } 94 | 95 | if ($params['filter']) { 96 | $pFilter = array(); 97 | foreach ($params['filter'] as $field => $value) { 98 | $field = preg_replace_callback("/\w+/", function ($matches) use ($modelToDb) { 99 | return $modelToDb[$matches[0]]; 100 | }, $field); 101 | $pFilter[$field] = $value; 102 | } 103 | $params['filter'] = $pFilter; 104 | } 105 | $dbResult = static::callGatewayMethod('getList', $params); 106 | $rows = $dbResult->fetchAll(); 107 | $items = array(); 108 | foreach ($rows as $row) { 109 | $items[] = self::createByRow($row); 110 | } 111 | return $items; 112 | } 113 | 114 | /** 115 | * @param array $params 116 | * @return $this 117 | */ 118 | static public function findOne($params = array()) { 119 | $cacheKey = md5(get_called_class().serialize($params)); 120 | if (!self::$_oneRequestsCache[$cacheKey]) { 121 | $params['limit'] = 1; 122 | $items = self::find($params); 123 | self::$_oneRequestsCache[$cacheKey] = $items[0]; 124 | } 125 | return self::$_oneRequestsCache[$cacheKey]; 126 | } 127 | 128 | /** 129 | * @return mixed 130 | * @internal param $p1 131 | * @internal param $p2 132 | * @internal param $p3 133 | * 134 | */ 135 | static public function callGatewayMethod() { 136 | $params = func_get_args(); 137 | $name = array_shift($params); 138 | return call_user_func_array(array(static::gatewayClass(), $name), $params); 139 | } 140 | 141 | public function delete() { 142 | $res = static::callGatewayMethod('delete', $this->id); 143 | return !(bool)$res->getErrors(); 144 | } 145 | 146 | public function insert() { 147 | $res = static::callGatewayMethod('add', $this->getRawFields()); 148 | $this->id = $res->getId(); 149 | $this->_errors = $res->getErrors() ?: array(); 150 | $this->isNew = false; 151 | return !(bool)$res->getErrors(); 152 | } 153 | 154 | public function update() { 155 | $res = static::callGatewayMethod('update', $this->id, $this->getRawFields()); 156 | $this->_errors = $res->getErrors() ?: array(); 157 | return !(bool)$res->getErrors(); 158 | } 159 | 160 | public function save() { 161 | return $this->isNew ? $this->insert() : $this->update(); 162 | } 163 | 164 | public function getErrors() { 165 | return $this->_errors; 166 | } 167 | 168 | /** 169 | * @throws NotImplementedException 170 | * @return array 171 | */ 172 | static protected function map() { 173 | throw new NotImplementedException('You should implement method `map`'); 174 | } 175 | 176 | /** 177 | * @throws NotImplementedException 178 | * @return string 179 | */ 180 | static protected function gatewayClass() { 181 | throw new NotImplementedException('You should implement method `gatewayClass`'); 182 | } 183 | 184 | static protected function modifyFromDb($data) { 185 | return $data; 186 | } 187 | 188 | static protected function modifyToDb($data) { 189 | return $data; 190 | } 191 | } -------------------------------------------------------------------------------- /lib/entities/setuplog.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\ReduceMigrations\Entities; 7 | 8 | 9 | use Bitrix\Main\Entity\DataManager; 10 | 11 | class SetupLogTable extends DataManager { 12 | 13 | public static function className() { 14 | return get_called_class(); 15 | } 16 | 17 | public static function getTableName() { 18 | return 'ws_reducemigrations_setups_log'; 19 | } 20 | 21 | /** 22 | * @return string 23 | */ 24 | public static function getFilePath() { 25 | return __FILE__; 26 | } 27 | 28 | public static function getMap() { 29 | return array( 30 | 'ID' => array( 31 | 'data_type' => 'integer', 32 | 'primary' => true, 33 | 'autocomplete' => true 34 | ), 35 | 'DATE' => array( 36 | 'data_type' => 'datetime', 37 | 'required' => true, 38 | ), 39 | 'USER_ID' => array( 40 | 'data_type' => 'integer' 41 | ) 42 | ); 43 | } 44 | } -------------------------------------------------------------------------------- /lib/entities/setuplogmodel.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\ReduceMigrations\Entities; 7 | 8 | use Bitrix\Main\Type\DateTime; 9 | use Bitrix\Main\UserTable; 10 | use WS\ReduceMigrations\factories\DateTimeFactory; 11 | 12 | class SetupLogModel extends BaseEntity { 13 | public 14 | $id, $userId; 15 | /** 16 | * @var \DateTime 17 | */ 18 | public $date; 19 | 20 | private $userData = false; 21 | 22 | public function __construct() { 23 | $this->date = DateTimeFactory::createBase(); 24 | } 25 | 26 | static protected function map() { 27 | return array( 28 | 'id' => 'ID', 29 | 'date' => 'DATE', 30 | 'userId' => 'USER_ID' 31 | ); 32 | } 33 | 34 | static protected function gatewayClass() { 35 | return SetupLogTable::className(); 36 | } 37 | 38 | static public function deleteById($id) { 39 | return self::callGatewayMethod('delete', $id); 40 | } 41 | 42 | static protected function modifyFromDb($data) { 43 | if ($data['date'] instanceof DateTime) { 44 | $timestamp = $data['date']->getTimestamp(); 45 | $data['date'] = DateTimeFactory::createBase(); 46 | $data['date']->setTimestamp($timestamp); 47 | } else { 48 | $data['date']= DateTimeFactory::createBase($data['date']); 49 | } 50 | return $data; 51 | } 52 | 53 | static protected function modifyToDb($data) { 54 | $data['date'] && $data['date'] instanceof \DateTime && $data['date'] = DateTimeFactory::createBitrix($data['date']); 55 | return $data; 56 | } 57 | 58 | /** 59 | * @return AppliedChangesLogModel[] 60 | */ 61 | public function getAppliedLogs() { 62 | return AppliedChangesLogModel::find(array( 63 | 'order' => array('id' => 'desc'), 64 | 'filter' => array( 65 | '=setupLogId' => $this->id 66 | ) 67 | )); 68 | } 69 | 70 | /** 71 | * @return array 72 | */ 73 | private function getUserData() { 74 | if ($this->userData === false) { 75 | $this->userData = UserTable::getById($this->userId)->fetch(); 76 | } 77 | return $this->userData; 78 | } 79 | 80 | public function shortUserInfo() { 81 | $res = 'cli'; 82 | if ($this->userId) { 83 | $data = $this->getUserData(); 84 | $res = $data['NAME'].' '.$data['LAST_NAME']; 85 | } 86 | return $res; 87 | } 88 | } -------------------------------------------------------------------------------- /lib/exceptions/multipleequalhashexception.php: -------------------------------------------------------------------------------- 1 | format($format), $format, self::timeZone()); 23 | return $object; 24 | } 25 | 26 | /** 27 | * @return \DateTimeZone 28 | */ 29 | public static function timeZone() { 30 | try { 31 | $obj = new \DateTime(); 32 | return $obj->getTimezone(); 33 | } catch (\Exception $e) { 34 | date_default_timezone_set(self::DEFAULT_TIME_ZONE); 35 | return new \DateTimeZone(self::DEFAULT_TIME_ZONE); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /lib/localization.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class Localization { 9 | 10 | /** 11 | * @var Options 12 | */ 13 | private $data; 14 | 15 | public function __construct($data) { 16 | $this->data = new Options($data); 17 | } 18 | 19 | /** 20 | * @return Options 21 | */ 22 | private function getData() { 23 | return $this->data; 24 | } 25 | 26 | /** 27 | * @param string $path @see Options 28 | * @param array $replace 29 | * @return mixed 30 | */ 31 | public function message($path, $replace = null) { 32 | $m = $this->getData()->get($path, ''); 33 | $result = $m ?: $path; 34 | if (is_array($replace)) { 35 | $result = str_replace(array_keys($replace), array_values($replace), $m); 36 | } 37 | return $result; 38 | } 39 | 40 | /** 41 | * @param string $path @see Options 42 | * @return Localization 43 | */ 44 | public function fork($path) { 45 | return new static($this->getData()->get($path)); 46 | } 47 | 48 | public function getDataByPath($path) { 49 | return $this->getData()->get($path, ''); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/messageoutputinterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | final class ModuleOptions { 11 | private $moduleName = 'ws.reducemigrations'; 12 | 13 | private $cache = array(); 14 | 15 | /** 16 | * @staticvar self $self 17 | * @return ModuleOptions 18 | */ 19 | static public function getInstance() { 20 | static $self = null; 21 | if (!$self) { 22 | $self = new self; 23 | } 24 | return $self; 25 | } 26 | 27 | private function setToDb($name, $value) { 28 | \COption::SetOptionString($this->moduleName, $name, serialize($value)); 29 | } 30 | 31 | private function getFromDb($name) { 32 | $value = \COption::GetOptionString($this->moduleName, $name); 33 | return unserialize($value); 34 | } 35 | 36 | public function __set($name, $value) { 37 | $this->setToCache($name, $value); 38 | $this->setToDb($name, $value); 39 | return $value; 40 | } 41 | 42 | public function __get($name) { 43 | $value = $this->getFormCache($name); 44 | if (is_null($value)) { 45 | $value = $this->getFromDb($name); 46 | $this->setToCache($name, $value); 47 | } 48 | return $value; 49 | } 50 | 51 | /** 52 | * @param $name 53 | * @return mixed 54 | */ 55 | private function getFormCache($name) { 56 | return $this->cache[$name]; 57 | } 58 | 59 | /** 60 | * @param $name 61 | * @param $value 62 | */ 63 | private function setToCache($name, $value) { 64 | $this->cache[$name] = $value; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /lib/options.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class Options implements \Serializable, \ArrayAccess { 9 | 10 | private $data = array(); 11 | 12 | public function __construct(array $data = null) { 13 | $data && ($this->data = $data); 14 | } 15 | 16 | /** 17 | * @param string $path 18 | * @param mixed $default 19 | * 20 | * @throws \Exception 21 | * @return mixed 22 | */ 23 | public function get($path, $default = null) { 24 | 25 | $usesAliases = array(); 26 | $rPath = preg_replace_callback('/\[.*?\]/', function ($matches) use (& $usesAliases) { 27 | $key = trim($matches[0], '[]'); 28 | $alias = str_replace('.', '_', $key); 29 | $usesAliases[$alias] = $key; 30 | 31 | return '.' . $alias; 32 | }, $path); 33 | 34 | $arPath = explode('.', $rPath); 35 | $data = $this->data; 36 | while (($pathItem = array_shift($arPath)) !== null) { 37 | if ($usesAliases[$pathItem]) { 38 | $pathItem = $usesAliases[$pathItem]; 39 | unset($usesAliases[$pathItem]); 40 | } 41 | 42 | if ($data instanceof self) { 43 | $data = $data->toArray(); 44 | } 45 | if (!isset($data[$pathItem])) { 46 | if (!is_null($default)) { 47 | return $default; 48 | } 49 | throw new \Exception("Value by path `$path` not exist"); 50 | } 51 | $data = $data[$pathItem]; 52 | } 53 | 54 | return $data; 55 | } 56 | 57 | /** 58 | * @param string $path 59 | * @param null|mixed $default 60 | * 61 | * @throws \Exception 62 | * @return $this 63 | */ 64 | public function getAsObject($path, $default = null) { 65 | $res = $this->get($path, $default); 66 | if (!is_array($res)) { 67 | throw new \Exception("Return value as object not available"); 68 | } 69 | 70 | return new static($this->get($path)); 71 | } 72 | 73 | /** 74 | * @param \ArrayAccess|array $mergedOptions 75 | * 76 | * @return Options 77 | */ 78 | public function merge($mergedOptions) { 79 | if (is_object($mergedOptions) && $mergedOptions instanceof Options) { 80 | $mergedOptions = $mergedOptions->toArray(); 81 | } 82 | foreach ($mergedOptions as $path => $value) { 83 | $this->set($path, $value); 84 | } 85 | 86 | return $this; 87 | } 88 | 89 | /** 90 | * @param string $path 91 | * @param mixed $value 92 | * 93 | * @throws \Exception 94 | * @return Options 95 | */ 96 | public function set($path, $value) { 97 | $arPath = explode('.', $path); 98 | $data = &$this->data; 99 | while (($key = array_shift($arPath)) !== null) { 100 | if (empty($arPath)) { 101 | $key ? $data[$key] = $value : $data[] = $value; 102 | } else { 103 | if (!$key) { 104 | throw new \Exception('Need last iterated by path. Available: ' . $path); 105 | } 106 | if (!isset($data[$key])) { 107 | $data[$key] = array(); 108 | } 109 | $data = &$data[$key]; 110 | } 111 | } 112 | 113 | return $this; 114 | } 115 | 116 | public function __invoke() { 117 | $args = func_get_args(); 118 | switch (count($args)) { 119 | case 1: 120 | return $this->get($args[0]); 121 | break; 122 | case 2: 123 | return $this->set($args[0], $args[1]); 124 | break; 125 | } 126 | } 127 | 128 | public function toArray() { 129 | return $this->data; 130 | } 131 | 132 | public function serialize() { 133 | return serialize($this->data); 134 | } 135 | 136 | public function unserialize($serialized) { 137 | $this->data = unserialize($serialized); 138 | } 139 | 140 | public function offsetExists($offset) { 141 | try { 142 | $this->get($offset); 143 | return true; 144 | } catch (\Exception $e) { 145 | return false; 146 | } 147 | } 148 | 149 | public function offsetGet($offset) { 150 | return $this->get($offset); 151 | } 152 | 153 | public function offsetSet($offset, $value) { 154 | $this->set($offset, $value); 155 | 156 | return $value; 157 | } 158 | 159 | public function offsetUnset($offset) { 160 | $this->set($offset, null); 161 | } 162 | 163 | public function toJson() { 164 | return json_encode($this->toArray()); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /lib/scenario/exceptions/applyscenarioexception.php: -------------------------------------------------------------------------------- 1 | setData($data); 41 | if ($printer === null) { 42 | $printer = new DumbMessageOutput(); 43 | } 44 | $this->printer = $printer; 45 | } 46 | 47 | /** 48 | * @return array 49 | */ 50 | public function getData() { 51 | return $this->data; 52 | } 53 | 54 | /** 55 | * @param array $value 56 | */ 57 | public function setData(array $value = array()) { 58 | $this->data = $value; 59 | } 60 | 61 | /** 62 | * @param $key 63 | * @param $value 64 | */ 65 | public function setDataByKey($key, $value) { 66 | $this->data[$key] = $value; 67 | } 68 | 69 | /** 70 | * @param $key 71 | * 72 | * @return mixed|null 73 | */ 74 | public function getDataByKey($key) { 75 | return isset($this->data[$key]) ? $this->data[$key] : null; 76 | } 77 | 78 | /** 79 | * Check to valid class definition 80 | * 81 | * @return bool 82 | */ 83 | static public function isValid() { 84 | return static::name(); 85 | } 86 | 87 | static public function getShortenedHash() { 88 | return substr(static::hash(), 0 , self::SHORTENED_HASH_LENGTH); 89 | } 90 | /** 91 | * @return array 92 | */ 93 | public static function getPriorities() { 94 | return array( 95 | self::PRIORITY_HIGH, 96 | self::PRIORITY_MEDIUM, 97 | self::PRIORITY_OPTIONAL, 98 | ); 99 | } 100 | 101 | /** 102 | * @return bool 103 | */ 104 | public function isOptional() { 105 | return self::priority() === self::PRIORITY_OPTIONAL; 106 | } 107 | 108 | /** 109 | * Runs to commit migration 110 | */ 111 | abstract public function commit(); 112 | 113 | /** 114 | * Runs by rollback migration 115 | */ 116 | abstract public function rollback(); 117 | 118 | /** 119 | * Returns name of migration 120 | * 121 | * @return string 122 | */ 123 | public static function name() { 124 | return null; 125 | } 126 | 127 | /** 128 | * @return string - is hash 129 | */ 130 | public static function hash() { 131 | return null; 132 | } 133 | 134 | /** 135 | * @return int approximately time in seconds 136 | */ 137 | public static function approximatelyTime() { 138 | return 0; 139 | } 140 | 141 | /** 142 | * Returns priority of migration 143 | * 144 | * @return string 145 | */ 146 | public static function priority() { 147 | return self::PRIORITY_HIGH; 148 | } 149 | 150 | /** 151 | * @return MessageOutputInterface 152 | */ 153 | protected function printer() { 154 | return $this->printer; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /lib/tests/abstractcase.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/worksolutions/bitrix-reduce-migrations/bee31077f14846bb7accf1e60445a7c0ff1debd2/lib/tests/abstractcase.php -------------------------------------------------------------------------------- /lib/tests/cases/agentbuildercase.php: -------------------------------------------------------------------------------- 1 | localization->message('name'); 13 | } 14 | 15 | public function description() { 16 | return $this->localization->message('description'); 17 | } 18 | 19 | public function close() { 20 | $agent = \CAgent::GetList(null, array( 21 | 'NAME' => 'abs(0);' 22 | ))->Fetch(); 23 | \CAgent::Delete($agent['ID']); 24 | } 25 | 26 | 27 | public function testAdd() { 28 | $date = new DateTime(); 29 | $date->add('+1 day'); 30 | $builder = new \WS\ReduceMigrations\Builder\AgentBuilder(); 31 | $obAgent = $builder->addAgent('abs(0);', function (Agent $agent) use ($date) { 32 | $agent 33 | ->sort(23) 34 | ->active(true) 35 | ->nextExec($date); 36 | }); 37 | $agent = \CAgent::GetList(null, array( 38 | 'ID' => $obAgent->getId() 39 | ))->Fetch(); 40 | 41 | $this->assertNotEmpty($agent); 42 | $this->assertEquals($agent['NAME'], 'abs(0);'); 43 | $this->assertEquals($agent['SORT'], 23); 44 | $this->assertEquals($agent['ACTIVE'], "Y"); 45 | $this->assertEquals($agent['NEXT_EXEC'], $date->format('d.m.Y H:i:s')); 46 | } 47 | 48 | 49 | public function testUpdate() { 50 | $builder = new \WS\ReduceMigrations\Builder\AgentBuilder(); 51 | $obAgent = $builder 52 | ->updateAgent('abs(0);', function (Agent $agent) { 53 | $agent 54 | ->active(false) 55 | ->isPeriod(true); 56 | }); 57 | 58 | $agent = \CAgent::GetList(null, array( 59 | 'ID' => $obAgent->getId() 60 | ))->Fetch(); 61 | 62 | $this->assertNotEmpty($agent); 63 | $this->assertEquals($agent['ACTIVE'], 'N'); 64 | $this->assertEquals($agent['IS_PERIOD'], 'Y'); 65 | } 66 | 67 | } -------------------------------------------------------------------------------- /lib/tests/cases/errorexception.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\ReduceMigrations\Tests\Cases; 7 | 8 | 9 | class ErrorException extends \Exception 10 | { 11 | 12 | private $_dumpedValue; 13 | 14 | public function setDump($value) { 15 | $this->_dumpedValue = $value; 16 | } 17 | 18 | public function getDump() { 19 | return $this->_dumpedValue; 20 | } 21 | } -------------------------------------------------------------------------------- /lib/tests/cases/eventsbuildercase.php: -------------------------------------------------------------------------------- 1 | localization->message('name'); 15 | } 16 | 17 | public function description() { 18 | return $this->localization->message('description'); 19 | } 20 | 21 | public function close() { 22 | $eventType = \CEventType::GetList(array( 23 | 'TYPE_ID' => 'WS_MIGRATION_TEST_EVENT', 24 | 'LID' => 'en' 25 | ))->Fetch(); 26 | $gw = new \CEventType; 27 | $gw->Delete($eventType['ID']); 28 | } 29 | 30 | public function testAdd() { 31 | $builder = new EventsBuilder(); 32 | $builder->createEventType('WS_MIGRATION_TEST_EVENT', 'ru', function (EventType $event) { 33 | $event 34 | ->name('Тестовое событие миграций') 35 | ->sort(10) 36 | ->description('#TEST# - test'); 37 | $event 38 | ->addEventMessage('#EMAIL_FROM#', '#EMAIL_TO#', 's1') 39 | ->subject('Hello') 40 | ->body('Hello #TEST#!') 41 | ->bodyType(EventMessage::BODY_TYPE_HTML) 42 | ->active(true) 43 | ; 44 | $event 45 | ->addEventMessage('#FROM#', '#TO#', 's1') 46 | ->subject('Hi') 47 | ->body('Hi #TEST#!') 48 | ->active(false) 49 | ->bodyType(EventMessage::BODY_TYPE_TEXT) 50 | ; 51 | }); 52 | 53 | $eventType = \CEventType::GetList(array( 54 | 'TYPE_ID' => 'WS_MIGRATION_TEST_EVENT', 55 | 'LID' => 'ru' 56 | ))->Fetch(); 57 | 58 | $this->assertNotEmpty($eventType); 59 | $this->assertEquals($eventType['SORT'], 10); 60 | $this->assertNotEmpty($eventType['DESCRIPTION'], '#TEST# - test'); 61 | $this->assertNotEmpty($eventType['NAME'], 'Тестовое событие миграций'); 62 | 63 | $res = EventMessageTable::getList(array( 64 | 'filter' => array( 65 | 'EVENT_NAME' => 'WS_MIGRATION_TEST_EVENT' 66 | ) 67 | )); 68 | $this->assertEquals($res->getSelectedRowsCount(), 2); 69 | while ($item = $res->fetch()) { 70 | if ($item['SUBJECT'] == 'Hi') { 71 | $this->assertEquals($item['BODY_TYPE'], 'text'); 72 | $this->assertEquals($item['MESSAGE'], 'Hi #TEST#!'); 73 | $this->assertEquals($item['LID'], 's1'); 74 | $this->assertEquals($item['ACTIVE'], 'N'); 75 | $this->assertEquals($item['EMAIL_FROM'], '#FROM#'); 76 | $this->assertEquals($item['EMAIL_TO'], '#TO#'); 77 | } 78 | } 79 | } 80 | 81 | 82 | public function testUpdate() { 83 | $builder = new EventsBuilder(); 84 | $builder->updateEventType('WS_MIGRATION_TEST_EVENT', 'ru', function (EventType $type) { 85 | $type 86 | ->lid('en') 87 | ->name('Тестовое событие'); 88 | 89 | foreach ($type->loadEventMessages() as $message) { 90 | if ($message->getAttribute('SUBJECT') == 'Hello') { 91 | $message->remove(); 92 | } 93 | $message->bcc('#BCC#'); 94 | } 95 | }); 96 | 97 | $eventType = \CEventType::GetList(array( 98 | 'TYPE_ID' => 'WS_MIGRATION_TEST_EVENT', 99 | 'LID' => 'en' 100 | ))->Fetch(); 101 | $this->assertTrue(!empty($eventType)); 102 | $this->assertNotEmpty($eventType['NAME'], 'Тестовое событие'); 103 | 104 | $res = EventMessageTable::getList(array( 105 | 'filter' => array( 106 | 'EVENT_NAME' => 'WS_MIGRATION_TEST_EVENT' 107 | ) 108 | )); 109 | $this->assertEquals($res->getSelectedRowsCount(), 1); 110 | while ($item = $res->fetch()) { 111 | $this->assertEquals($item['BCC'], '#BCC#'); 112 | } 113 | } 114 | 115 | } -------------------------------------------------------------------------------- /lib/tests/cases/formbuildercase.php: -------------------------------------------------------------------------------- 1 | localization->message('name'); 14 | } 15 | 16 | public function description() { 17 | return $this->localization->message('description'); 18 | } 19 | 20 | public function init() { 21 | \CModule::IncludeModule('form'); 22 | } 23 | 24 | public function close() { 25 | $form = \CForm::GetList($by, $order, array( 26 | 'SID' => 'TestForm', 27 | ), $isFiltered)->Fetch(); 28 | if (!$form) { 29 | return; 30 | } 31 | \CForm::Delete($form['ID'], 'N'); 32 | } 33 | 34 | public function testAdd() { 35 | $builder = new FormBuilder(); 36 | $newForm = $builder->addForm('TestForm', 'TestForm', function (Form $form) { 37 | $form 38 | ->arSiteId(array('s1')) 39 | ->sort(10) 40 | ->description('Description') 41 | ->useCaptcha(true) 42 | ->arGroup(array( 43 | '2' => 10 44 | )) 45 | ->arMenu(array("ru" => "Анкета посетителя", "en" => "Visitor Form")) 46 | ->descriptionType('html'); 47 | 48 | $form 49 | ->addField('testQuestion') 50 | ->fieldType(FormField::FIELD_TYPE_INTEGER) 51 | ->sort(33) 52 | ->active(false) 53 | ->required(true) 54 | ->title('testTitle') 55 | ->arFilterAnswerText(array("dropdown")) 56 | ->arFilterAnswerValue(array("dropdown")) 57 | ->arFilterUser(array("dropdown")) 58 | ->arFilterField(array("integer")) 59 | ->comments('test comment') 60 | ->addAnswer('Привет мир!'); 61 | 62 | $form 63 | ->addField('testField') 64 | ->asField() 65 | ->title('test') 66 | ; 67 | 68 | $form 69 | ->addStatus('status') 70 | ->arGroupCanDelete(array(2)) 71 | ->byDefault(true); 72 | }); 73 | 74 | 75 | $form = \CForm::GetList($by, $order, array( 76 | 'ID' => $newForm->getId(), 77 | ), $isFiltered)->Fetch(); 78 | 79 | $this->assertNotEmpty($form); 80 | $this->assertEquals($form['C_SORT'], 10); 81 | $this->assertNotEmpty($form['DESCRIPTION'], 'Description'); 82 | $this->assertNotEmpty($form['NAME'], 'TestForm'); 83 | $this->assertNotEmpty($form['USE_CAPTCHA'], 'Y'); 84 | 85 | $res = \CFormField::GetList($newForm->getId(), 'ALL', $by, $order, array(), $isFiltered); 86 | 87 | $this->assertEquals($res->SelectedRowsCount(), 2); 88 | while ($item = $res->fetch()) { 89 | if ($item['SID'] == 'testQuestion') { 90 | $this->assertEquals($item['ACTIVE'], 'N'); 91 | $this->assertEquals($item['ADDITIONAL'], 'N'); 92 | $this->assertEquals($item['FIELD_TYPE'], 'integer'); 93 | $this->assertEquals($item['TITLE'], 'testTitle'); 94 | $this->assertEquals($item['C_SORT'], 33); 95 | $this->assertEquals($item['REQUIRED'], 'Y'); 96 | $this->assertEquals($item['COMMENTS'], 'test comment'); 97 | } 98 | if ($item['SID'] == 'testField') { 99 | $this->assertEquals($item['ADDITIONAL'], 'Y'); 100 | $this->assertEquals($item['TITLE'], 'test'); 101 | } 102 | } 103 | 104 | $res = \CFormStatus::GetList($newForm->getId(), $by, $order, array(), $isFiltered)->Fetch(); 105 | $this->assertEquals($res['TITLE'], 'status'); 106 | $this->assertEquals($res['DEFAULT_VALUE'], 'Y'); 107 | } 108 | 109 | 110 | public function testUpdate() { 111 | $builder = new FormBuilder(); 112 | $updatedForm = $builder->updateForm('TestForm', function (Form $form) { 113 | $form->name('MyTestForm'); 114 | $field = $form 115 | ->updateField('testQuestion') 116 | ->active(true) 117 | ->required(false); 118 | 119 | $field->removeAnswer('Привет мир!'); 120 | 121 | $field 122 | ->addAnswer('Test') 123 | ->value('val1'); 124 | 125 | $form 126 | ->updateStatus('status') 127 | ->description('test22') 128 | ->arGroupCanDelete(array(2, 3)); 129 | }); 130 | 131 | $form = \CForm::GetList($by, $order, array( 132 | 'ID' => $updatedForm->getId(), 133 | ), $isFiltered)->Fetch(); 134 | 135 | $this->assertNotEmpty($form); 136 | $this->assertNotEmpty($form['NAME'], 'MyTestForm'); 137 | 138 | $res = \CFormField::GetList($updatedForm->getId(), 'ALL', $by, $order, array(), $isFiltered); 139 | 140 | $this->assertEquals($res->SelectedRowsCount(), 2); 141 | while ($item = $res->fetch()) { 142 | if ($item['SID'] == 'testQuestion') { 143 | $this->assertEquals($item['ACTIVE'], 'Y'); 144 | $this->assertEquals($item['ADDITIONAL'], 'N'); 145 | $this->assertEquals($item['FIELD_TYPE'], 'integer'); 146 | $this->assertEquals($item['TITLE'], 'testTitle'); 147 | $this->assertEquals($item['REQUIRED'], 'N'); 148 | $this->assertEquals($item['COMMENTS'], 'test comment'); 149 | } 150 | } 151 | 152 | $res = \CFormStatus::GetList($updatedForm->getId(), $by, $order, array(), $isFiltered)->Fetch(); 153 | $this->assertEquals($res['DESCRIPTION'], 'test22'); 154 | } 155 | 156 | } -------------------------------------------------------------------------------- /lib/tests/cases/highloadblockbuildercase.php: -------------------------------------------------------------------------------- 1 | localization->message('name'); 15 | } 16 | 17 | public function description() { 18 | return $this->localization->message('description'); 19 | } 20 | 21 | public function close() { 22 | $arIblock = HighloadBlockTable::getList(array( 23 | 'filter' => array( 24 | 'TABLE_NAME' => 'test_highloadblock' 25 | ) 26 | ))->fetch(); 27 | 28 | HighloadBlockTable::delete($arIblock['ID']); 29 | } 30 | 31 | 32 | public function testAdd() { 33 | $builder = new HighLoadBlockBuilder(); 34 | $block = $builder->addHLBlock('TestBlock', 'test_highloadblock', function (HighLoadBlock $block) { 35 | $prop = $block 36 | ->addField('uf_test1') 37 | ->sort(10) 38 | ->label(array('ru' => 'Тест')) 39 | ->type(UserField::TYPE_ENUMERATION); 40 | 41 | $prop->addEnum('Тест1'); 42 | $prop->addEnum('Тест2'); 43 | $prop->addEnum('Тест3'); 44 | 45 | $block 46 | ->addField('uf_test2') 47 | ->label(array('ru' => 'Тест2')) 48 | ->type(UserField::TYPE_HLBLOCK); 49 | 50 | $block 51 | ->addField('uf_test3') 52 | ->label(array('ru' => 'Тест2')) 53 | ->type(UserField::TYPE_BOOLEAN); 54 | 55 | $block 56 | ->addField('uf_test4') 57 | ->label(array('ru' => 'Тест2')) 58 | ->type(UserField::TYPE_DATETIME); 59 | 60 | $block 61 | ->addField('uf_test5') 62 | ->label(array('ru' => 'Тест2')) 63 | ->type(UserField::TYPE_IBLOCK_ELEMENT); 64 | 65 | $block 66 | ->addField('uf_test6') 67 | ->label(array('ru' => 'Тест2')) 68 | ->type(UserField::TYPE_VOTE); 69 | 70 | $block 71 | ->addField('uf_test7') 72 | ->label(array('ru' => 'Тест2')) 73 | ->type(UserField::TYPE_VIDEO); 74 | 75 | $block 76 | ->addField('uf_test8') 77 | ->label(array('ru' => 'Тест2')) 78 | ->type(UserField::TYPE_IBLOCK_SECTION); 79 | }); 80 | 81 | 82 | $arIblock = HighloadBlockTable::getList(array( 83 | 'filter' => array( 84 | 'ID' => $block->getId() 85 | ) 86 | ))->fetch(); 87 | 88 | $this->assertNotEmpty($arIblock, "hlblock wasn't created"); 89 | $this->assertEquals($arIblock['TABLE_NAME'], $block->getAttribute('TABLE_NAME')); 90 | $this->assertEquals($arIblock['NAME'], $block->getAttribute('NAME')); 91 | 92 | $fields = \CUserTypeEntity::GetList(null, array( 93 | 'ENTITY_ID' => "HLBLOCK_" . $block->getId(), 94 | )); 95 | 96 | $this->assertEquals($fields->SelectedRowsCount(), 8); 97 | while ($field = $fields->Fetch()) { 98 | $field['NAME'] == 'uf_test5' && $this->assertEquals($field['USER_TYPE_ID'], UserField::TYPE_IBLOCK_ELEMENT); 99 | } 100 | 101 | } 102 | 103 | public function testUpdate() { 104 | $builder = new HighLoadBlockBuilder(); 105 | $block = $builder->updateHLBlock('test_highloadblock', function (HighLoadBlock $block) { 106 | $block->name('TestBlock2'); 107 | $prop = $block 108 | ->updateField('uf_test1') 109 | ->multiple(true) 110 | ->required(true); 111 | $prop->updateEnum('Тест1')->xmlId('test1'); 112 | $prop->removeEnum('Тест2'); 113 | }); 114 | $prop = $block->updateField('uf_test1'); 115 | 116 | $arIblock = HighloadBlockTable::getList(array( 117 | 'filter' => array( 118 | 'ID' => $block->getId() 119 | ) 120 | ))->fetch(); 121 | 122 | $this->assertEquals($arIblock['NAME'], $block->getAttribute('NAME')); 123 | 124 | $res = \CUserFieldEnum::GetList(null, array( 125 | 'USER_FIELD_ID' => $block->getId(), 126 | 'VALUE' => 'Тест2', 127 | ))->Fetch(); 128 | 129 | $this->assertEmpty($res); 130 | 131 | $res = \CUserFieldEnum::GetList(null, array( 132 | 'USER_FIELD_ID' => $prop->getId(), 133 | 'VALUE' => 'Тест1', 134 | ))->Fetch(); 135 | 136 | $this->assertNotEmpty($res); 137 | $this->assertEquals($res['XML_ID'], 'test1'); 138 | } 139 | 140 | } -------------------------------------------------------------------------------- /lib/tests/cases/tablebuildercase.php: -------------------------------------------------------------------------------- 1 | localization->message('name'); 14 | } 15 | 16 | public function description() { 17 | return $this->localization->message('description'); 18 | } 19 | 20 | public function close() { 21 | 22 | } 23 | 24 | public function testAlgorithm() { 25 | $this->add(); 26 | $this->update(); 27 | $this->drop(); 28 | } 29 | 30 | 31 | public function add() { 32 | $tableBuilder = new TableBuilder(); 33 | $tableBuilder->create('test_reducemigrations_table', function (Table $table) { 34 | $table->integer('ID') 35 | ->autoincrement(true) 36 | ->primary(true); 37 | $table->string('NAME'); 38 | $table->text('ABOUT'); 39 | $table->date('BIRTHDAY'); 40 | }); 41 | 42 | $tableInfo = $this->getTableInfo('test_reducemigrations_table'); 43 | 44 | $this->assertNotEmpty($tableInfo); 45 | $this->assertEquals($tableInfo['ID']['Type'], 'int(11)'); 46 | $this->assertEquals($tableInfo['ID']['Key'], 'PRI'); 47 | $this->assertEquals($tableInfo['ID']['Extra'], 'auto_increment'); 48 | $this->assertEquals($tableInfo['NAME']['Type'], 'varchar(255)'); 49 | $this->assertEquals($tableInfo['ABOUT']['Type'], 'text'); 50 | $this->assertEquals($tableInfo['BIRTHDAY']['Type'], 'date'); 51 | } 52 | 53 | 54 | public function update() { 55 | $tableBuilder = new TableBuilder(); 56 | $tableBuilder->addColumn('test_reducemigrations_table', 'TEST_COLUMN', 'LONGTEXT'); 57 | $tableBuilder->dropColumn('test_reducemigrations_table', 'BIRTHDAY'); 58 | 59 | $tableInfo = $this->getTableInfo('test_reducemigrations_table'); 60 | 61 | $this->assertNotEmpty($tableInfo); 62 | $this->assertEquals($tableInfo['ID']['Type'], 'int(11)'); 63 | $this->assertEquals($tableInfo['ID']['Key'], 'PRI'); 64 | $this->assertEquals($tableInfo['ID']['Extra'], 'auto_increment'); 65 | $this->assertEquals($tableInfo['NAME']['Type'], 'varchar(255)'); 66 | $this->assertEquals($tableInfo['ABOUT']['Type'], 'text'); 67 | $this->assertEquals($tableInfo['TEST_COLUMN']['Type'], 'longtext'); 68 | $this->assertTrue(!isset($tableInfo['BIRTHDAY'])); 69 | } 70 | 71 | public function drop() { 72 | $tableBuilder = new TableBuilder(); 73 | $tableBuilder->drop('test_reducemigrations_table'); 74 | 75 | try { 76 | $this->getTableInfo('test_reducemigrations_table'); 77 | $this->assertTrue(false);//table wasn't deleted 78 | } catch (\Bitrix\Main\DB\SqlQueryException $e) { 79 | //everything ok 80 | } 81 | } 82 | 83 | private function getTableInfo($tableName) { 84 | $database = Application::getConnection(); 85 | $res = $database->query("DESCRIBE $tableName"); 86 | $info = array(); 87 | while ($item = $res->fetch()) { 88 | $info[$item['Field']] = $item; 89 | } 90 | 91 | return $info; 92 | } 93 | 94 | } -------------------------------------------------------------------------------- /lib/tests/result.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\ReduceMigrations\Tests; 7 | 8 | 9 | class Result { 10 | private $success; 11 | private $message; 12 | private $trace; 13 | 14 | /** 15 | * @return mixed 16 | */ 17 | public function getMessage() { 18 | return $this->message; 19 | } 20 | 21 | /** 22 | * @param mixed $value 23 | * @return $this 24 | */ 25 | public function setMessage($value) { 26 | $this->message = $value; 27 | return $this; 28 | } 29 | 30 | public function setTrace($aTrace) { 31 | $this->trace = $aTrace; 32 | return $this; 33 | } 34 | 35 | /** 36 | * @return mixed 37 | */ 38 | public function getTrace() { 39 | return $this->trace; 40 | } 41 | 42 | /** 43 | * @return mixed 44 | */ 45 | public function isSuccess() { 46 | return $this->success; 47 | } 48 | 49 | /** 50 | * @param mixed $value 51 | * @return $this 52 | */ 53 | public function setSuccess($value) { 54 | $this->success = $value; 55 | return $this; 56 | } 57 | 58 | /** 59 | * @return array 60 | */ 61 | public function toArray() { 62 | return array( 63 | 'STATUS' => $this->isSuccess(), 64 | 'MESSAGE' => array( 65 | 'PREVIEW' => str_replace("\n", "
", $this->getMessage()), 66 | 'DETAIL' => $this->getTrace() 67 | ) 68 | ); 69 | } 70 | 71 | } -------------------------------------------------------------------------------- /lib/tests/starter.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\ReduceMigrations\Tests; 7 | 8 | 9 | use WS\ReduceMigrations\Module; 10 | use WS\ReduceMigrations\Tests\Cases\AgentBuilderCase; 11 | use WS\ReduceMigrations\Tests\Cases\EventsBuilderCase; 12 | use WS\ReduceMigrations\Tests\Cases\FormBuilderCase; 13 | use WS\ReduceMigrations\Tests\Cases\HighLoadBlockBuilderCase; 14 | use WS\ReduceMigrations\Tests\Cases\IblockBuilderCase; 15 | use WS\ReduceMigrations\Tests\Cases\TableBuilderCase; 16 | 17 | class Starter { 18 | 19 | const SECTION = 'WSREDUCEMIGRATIONS'; 20 | 21 | static public function className() { 22 | return get_called_class(); 23 | } 24 | 25 | /** 26 | * @return \WS\ReduceMigrations\Localization 27 | */ 28 | static public function getLocalization() { 29 | return Module::getInstance()->getLocalization('tests'); 30 | } 31 | 32 | static public function cases() { 33 | return array( 34 | IblockBuilderCase::className(), 35 | HighLoadBlockBuilderCase::className(), 36 | AgentBuilderCase::className(), 37 | EventsBuilderCase::className(), 38 | FormBuilderCase::className(), 39 | TableBuilderCase::className(), 40 | ); 41 | } 42 | 43 | static private function getLocalizationByCase ($class) { 44 | return static::getLocalization()->fork('cases.'.$class); 45 | } 46 | 47 | /** 48 | * Run module tests 49 | * @internal param $aCheckList 50 | * @return array 51 | */ 52 | static public function items() { 53 | if (!Module::getInstance()->getOptions()->useAutotests) { 54 | return array(); 55 | } 56 | $points = array(); 57 | $i = 1; 58 | 59 | foreach (self::cases() as $caseClass) { 60 | /** @var $case AbstractCase */ 61 | $case = new $caseClass(static::getLocalizationByCase($caseClass)); 62 | $points[self::SECTION.'-'.$i++] = array( 63 | 'AUTO' => 'Y', 64 | 'NAME' => $case->name(), 65 | 'DESC' => $case->description(), 66 | 'CLASS_NAME' => get_called_class(), 67 | 'METHOD_NAME' => 'run', 68 | 'PARENT' => self::SECTION, 69 | 'PARAMS' => array( 70 | 'class' => $caseClass 71 | ) 72 | ); 73 | } 74 | 75 | return array( 76 | 'CATEGORIES' => array( 77 | self::SECTION => array( 78 | 'NAME' => static::getLocalization()->message('run.name') 79 | ) 80 | ), 81 | 'POINTS' => $points 82 | ); 83 | } 84 | 85 | static public function run($params) { 86 | $class = $params['class']; 87 | $result = new Result(); 88 | if (!$class) { 89 | $result->setSuccess(false); 90 | $result->setMessage('Params not is correct'); 91 | return $result->toArray(); 92 | } 93 | $testCase = new $class(static::getLocalizationByCase($class)); 94 | if (!$testCase instanceof AbstractCase) { 95 | $result->setSuccess(false); 96 | $result->setMessage('Case class is not correct'); 97 | return $result->toArray(); 98 | } 99 | $refClass = new \ReflectionObject($testCase); 100 | $testMethods = array_filter($refClass->getMethods(), function (\ReflectionMethod $method) { 101 | return strpos(strtolower($method->getName()), 'test') === 0; 102 | }); 103 | try { 104 | $count = 0; 105 | /** @var $method \ReflectionMethod */ 106 | $testCase->init(); 107 | foreach ($testMethods as $method) { 108 | $testCase->setUp(); 109 | $method->invoke($testCase); 110 | $testCase->tearDown(); 111 | $count++; 112 | } 113 | } catch (\Exception $e) { 114 | $result->setSuccess(false) 115 | ->setTrace($e->getTraceAsString()); 116 | $message = $method->getShortName(). ', '. $e->getMessage(); 117 | if ($e instanceof \WS\ReduceMigrations\Tests\Cases\ErrorException) { 118 | $e->getDump() && $message .= "\ndump: \n" . var_export($e->getDump(), true); 119 | } 120 | $result->setMessage($message); 121 | return $result->toArray(); 122 | } 123 | $testCase->close(); 124 | return $result->setSuccess(true) 125 | ->setMessage(static::getLocalization()->message('run.report.completed').':'.$count."\n".static::getLocalization()->message('run.report.assertions').': '.$testCase->getAssertsCount()) 126 | ->toArray(); 127 | } 128 | } -------------------------------------------------------------------------------- /lib/timeformatter.php: -------------------------------------------------------------------------------- 1 | 'min', 'seconds' => 'sec'] 18 | */ 19 | public function __construct($lang) { 20 | $this->lang = $lang; 21 | } 22 | 23 | public function format($initialTime) { 24 | $time = round($initialTime, self::SECONDS_PRECISION); 25 | if ($time >= self::SECONDS_THRESHOLD_VALUE) { 26 | $time = round($time / self::SECONDS_IN_MINUTE, self::MINUTES_PRECISION); 27 | $time = sprintf('%s %s', $time, $this->lang['minutes']); 28 | } else { 29 | $time = sprintf('%s %s', $time, $this->lang['seconds']); 30 | } 31 | return $time; 32 | } 33 | } -------------------------------------------------------------------------------- /lib/timer.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\ReduceMigrations; 7 | 8 | 9 | class Timer { 10 | 11 | private $time = 0; 12 | 13 | const TIME_PRECISION = 2; 14 | 15 | public function start() { 16 | $this->time = microtime(true); 17 | } 18 | 19 | public function stop() { 20 | $this->time = microtime(true) - $this->time; 21 | } 22 | 23 | public function getTime() { 24 | return round($this->time, self::TIME_PRECISION); 25 | } 26 | 27 | public function __toString() { 28 | return (string)$this->getTime(); 29 | } 30 | } -------------------------------------------------------------------------------- /options.php: -------------------------------------------------------------------------------- 1 | getLocalization('setup'); 8 | $options = $module->getOptions(); 9 | 10 | $errors = array(); 11 | 12 | $fCreateDir = function ($dir) { 13 | $parts = explode('/', $dir); 14 | $dir = rtrim($_SERVER['DOCUMENT_ROOT'], '/'); 15 | foreach ($parts as $part) { 16 | if (!$part) { 17 | continue; 18 | } 19 | $dir .= '/'.$part; 20 | if (!mkdir($dir)) { 21 | return false; 22 | } 23 | chmod($dir, 0777); 24 | } 25 | return true; 26 | }; 27 | 28 | $fSave = function ($data) use (& $errors, $module, $options, $localization, $fCreateDir) { 29 | 30 | $catalog = $data['catalog']; 31 | $catalogError = false; 32 | if (!$catalog) { 33 | $errors[] = $localization->getDataByPath('errors.catalogFieldEmpty'); 34 | $catalogError = true; 35 | } 36 | $dir = $_SERVER['DOCUMENT_ROOT'] .$catalog; 37 | if (!$catalogError && !is_dir($dir) && !$fCreateDir($catalog)) { 38 | $catalogError = true; 39 | $errors[] = $localization->getDataByPath('errors.notCreateDir'); 40 | } 41 | if (!$catalogError && !is_dir($dir)) { 42 | $errors[] = $localization->getDataByPath('errors.notCreateDir'); 43 | } elseif(!$catalogError) { 44 | $catalog && $options->catalogPath = $catalog; 45 | } 46 | 47 | $options->useAutotests = (bool)$data['tests']; 48 | }; 49 | 50 | $_POST['data'] && $fSave($_POST['data']); 51 | 52 | $errors && CAdminMessage::ShowMessage( 53 | array( 54 | "MESSAGE" => implode(', ', $errors), 55 | "TYPE" => "ERROR" 56 | ) 57 | ); 58 | $form = new CAdminForm('form', array( 59 | array( 60 | 'DIV' => 't1', 61 | 'TAB' => $localization->getDataByPath('tab'), 62 | ) 63 | )); 64 | echo BeginNote(); 65 | echo $localization->getDataByPath('description'); 66 | echo EndNote(); 67 | $form->Begin(array( 68 | 'FORM_ACTION' => $APPLICATION->GetCurUri() 69 | )); 70 | $form->BeginNextFormTab(); 71 | $form->AddEditField( 72 | 'data[catalog]', 73 | $localization->getDataByPath('fields.catalog'), 74 | true, 75 | array(), 76 | $options->catalogPath ?: '/bitrix/php_interface/reducemigrations' 77 | ); 78 | 79 | $form->Buttons(array('btnSave' => false, 'btnApply' => true)); 80 | $form->Show(); 81 | -------------------------------------------------------------------------------- /prolog.php: -------------------------------------------------------------------------------- 1 | DoInstall(array('catalog' => '/bitrix/php_interface/reducemigrations')); 14 | 15 | echo 'ok'; 16 | -------------------------------------------------------------------------------- /updater.php: -------------------------------------------------------------------------------- 1 | errorMessage[] = $mess; 13 | }; 14 | //===================================================== 15 | 16 | $docRoot = rtrim($_SERVER['DOCUMENT_ROOT'], '/').'/'; 17 | // install file platform version 18 | $uploadDir = $docRoot . \COption::GetOptionString("main", "upload_dir", "upload"); 19 | $modulePath = rtrim($docRoot, '/').$updater->kernelPath.'/modules/'.$updater->moduleID; 20 | $updatePath = $docRoot.$updater->curModulePath; 21 | 22 | $isInstalled = \Bitrix\Main\ModuleManager::isModuleInstalled($updater->moduleID); --------------------------------------------------------------------------------