├── .gitignore ├── .travis.yml ├── LICENSE.md ├── README.md ├── bin └── jedi ├── composer.json ├── docs └── ru │ ├── README.md │ ├── agent.md │ ├── application.md │ ├── cache.md │ ├── ci-env-init-command.jpg │ ├── ci.md │ ├── command.md │ ├── environment.md │ ├── install.md │ ├── module.md │ └── search.md ├── src ├── Agent │ ├── Agent.php │ ├── AgentHelper.php │ ├── AgentTask.php │ ├── AgentTrait.php │ └── Command │ │ ├── ExecuteCommand.php │ │ └── OnCronCommand.php ├── Application │ ├── Application.php │ ├── CanRestartTrait.php │ ├── Command │ │ ├── BitrixCommand.php │ │ ├── Command.php │ │ └── InitCommand.php │ └── Exception │ │ ├── BitrixException.php │ │ └── ConfigurationException.php ├── Cache │ └── Command │ │ └── ClearCommand.php ├── Environment │ └── Command │ │ └── InitCommand.php ├── Iblock │ ├── Command │ │ ├── ExportCommand.php │ │ ├── ImportCommand.php │ │ └── MigrationCommandTrait.php │ ├── Exception │ │ ├── ExportException.php │ │ ├── IblockException.php │ │ └── ImportException.php │ ├── Exporter.php │ ├── Importer.php │ └── MigrationInterface.php ├── Module │ ├── Command │ │ ├── LoadCommand.php │ │ ├── ModuleCommand.php │ │ ├── RegisterCommand.php │ │ ├── RemoveCommand.php │ │ ├── UnregisterCommand.php │ │ └── UpdateCommand.php │ ├── Exception │ │ ├── ModuleException.php │ │ ├── ModuleInstallException.php │ │ ├── ModuleLoadException.php │ │ ├── ModuleNotFoundException.php │ │ ├── ModuleUninstallException.php │ │ └── ModuleUpdateException.php │ └── Module.php └── Search │ └── Command │ └── ReIndexCommand.php ├── tests └── bootstrap.php └── tmpl ├── .jedi.php └── environments ├── dev └── config.php ├── index.php └── prod └── config.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | composer.lock -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: php 4 | 5 | php: 6 | - 5.4 7 | - 5.5 8 | - 5.6 9 | 10 | script: 11 | - composer self-update 12 | - composer install --prefer-source --no-interaction -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2016-2017 [Notamedia Ltd.](http://nota.media) 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Console Jedi 2 | 3 | [![Build Status](https://travis-ci.org/notamedia/console-jedi.svg)](https://travis-ci.org/notamedia/console-jedi) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/notamedia/console-jedi/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/notamedia/console-jedi/?branch=master) 5 | [![Latest Stable Version](https://poser.pugx.org/notamedia/console-jedi/v/stable)](https://packagist.org/packages/notamedia/console-jedi) 6 | [![Total Downloads](https://poser.pugx.org/notamedia/console-jedi/downloads)](https://packagist.org/packages/notamedia/console-jedi) 7 | [![License](https://poser.pugx.org/notamedia/console-jedi/license)](https://packagist.org/packages/notamedia/console-jedi) 8 | 9 | Console application for administration and support projects on Bitrix CMS. 10 | 11 | Features: 12 | 13 | * Continuous integration. 14 | * Environments settings. 15 | * Managing caching, modules, search system. 16 | * Nice API for creating agents. 17 | 18 | Made based on Symfony Console ♥. 19 | 20 | ## Installation 21 | 22 | ```bash 23 | composer require notamedia/console-jedi 24 | 25 | ./vendor/bin/jedi init 26 | ``` 27 | 28 | ## Documentation 29 | 30 | * [По-русски](docs/ru/README.md) -------------------------------------------------------------------------------- /bin/jedi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | run(); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "notamedia/console-jedi", 3 | "description": "Console application for CMS Bitrix", 4 | "keywords": [ 5 | "bitrix", 6 | "console", 7 | "application" 8 | ], 9 | "type": "library", 10 | "license": "MIT", 11 | "support": { 12 | "issues": "https://github.com/notamedia/console-jedi/issues", 13 | "source": "https://github.com/notamedia/console-jedi/" 14 | }, 15 | "require": { 16 | "php": ">=5.4", 17 | "symfony/console": "~2.8|~3", 18 | "symfony/filesystem": "~2.8|~3" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Notamedia\\ConsoleJedi\\": "src/" 23 | } 24 | }, 25 | "bin": [ 26 | "bin/jedi" 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /docs/ru/README.md: -------------------------------------------------------------------------------- 1 | # Документация по Console Jedi 2 | 3 | ## Введение 4 | 5 | * [Установка](install.md) 6 | * [Консольное приложение](application.md) 7 | * [Команды](command.md) 8 | 9 | ## Функционал «Джедая» 10 | 11 | * [Continuous Integration](ci.md) 12 | * [Настройки окружений](environment.md) 13 | * [Модули](module.md) 14 | * [Кеширование](cache.md) 15 | * [Агенты](agent.md) 16 | * [Поиск](search.md) 17 | 18 | --- 19 | 20 | Если вы найдёте неточность или сможете дополнить документацию, пожалуйста, форкнитесь и сделайте пул-реквест. 21 | -------------------------------------------------------------------------------- /docs/ru/agent.md: -------------------------------------------------------------------------------- 1 | # Агенты 2 | 3 | Тяжеловесные или отложенные операции на сайте удобно выполнять в фоновом режиме, иногда с определённой периодичностью, 4 | иногда практически моментально. Помочь в этом может [система очередей](http://ruhighload.com/index.php/2009/05/15/%D0%BE%D1%87%D0%B5%D1%80%D0%B5%D0%B4%D1%8C-%D1%81%D0%BE%D0%BE%D0%B1%D1%89%D0%B5%D0%BD%D0%B8%D0%B9-%D1%87%D1%82%D0%BE-%D1%8D%D1%82%D0%BE-%D0%B8-%D0%B7%D0%B0%D1%87%D0%B5%D0%BC/). 5 | Но в некоторых случаях, когда не требуется шардирование и отказоустойчивость подсистемы выполнения отложенных операций, 6 | можно воспользоваться штатной технологией «Битрикса» — [агентами](http://dev.1c-bitrix.ru/learning/course/?COURSE_ID=43&LESSON_ID=3436). 7 | К сожалению, эта технология имеет сырое API, но Console Jedi предоставляет свой API, покрывающий весь цикл работы с агентами. 8 | 9 | ## Жизненный цикл 10 | 11 | ### Настройка 12 | 13 | Переведите [выполнение агентов на cron](https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&LESSON_ID=2943&LESSON_PATH=3913.4776.4620.4978.2943), 14 | если ещё не сделали этого. В Console Jedi этот процесс автоматизирован, поэтому, в отличии от официального руководства, 15 | достаточно выполнить команду: 16 | 17 | ```bash 18 | vendor/bin/jedi agent:on-cron 19 | ``` 20 | 21 | ### Создание 22 | 23 | PHP-класс агента либо должен наследовать класс `\Notamedia\ConsoleJedi\Agent\Agent`, либо подключать трейт 24 | `\Notamedia\ConsoleJedi\Agent\AgentTrait`, позволяющие объекту класса работать в режиме агента. 25 | 26 | Вот и всё, больше не требуется никаких операций. И самое главное — не нужно создавать в классе статичные методы, как это 27 | рекомендует официальная документация «Битрикса», — Console Jedi умеет превращать объекты (с конструктором и публичными 28 | нестатичными методами) в формат, ожидаемый «Битриксом». 29 | 30 | ### Регистрация 31 | 32 | В момент, когда потребуется зарегистрировать в «Битриксе» новый агент, воспользуйтесь специальным строителем 33 | `\Notamedia\ConsoleJedi\Agent\AgentTask`. 34 | 35 | Давайте зарегистрируем агент модуля `vasya.tester`, при выполнении которого будет создаваться объект `TestAgent` с аргументами `arg1` 36 | (строка) и `true` (булевый тип) и вызываться метод объекта `execute` с аргументом `100500` (строка): 37 | 38 | ```php 39 | use Notamedia\ConsoleJedi\Agent\AgentTask; 40 | use Vendor\Module\TestAgent; 41 | 42 | AgentTask::builder() 43 | ->setClass(TestAgent::class) 44 | ->setConstructorArgs(['arg1', true]) 45 | ->setCallChain([ 46 | ['execute' => [100500]] 47 | ]), 48 | ->setModule('vasya.tester') 49 | ->create(); 50 | ``` 51 | 52 | > За подробным описанием методов строителя агентов обратитесь к PHPDoc-комментариям в классе `AgentTask`. 53 | 54 | ### Выполнение 55 | 56 | Добавьте в `crontab` вызов команды, которая производит выполнение готовых к работе агентов: 57 | 58 | ```bash 59 | vendor/bin/jedi agent:execute 60 | ``` 61 | 62 | ## Продление жизни агента 63 | 64 | Если время выполнения агента превышает 10 минут, необходимо периодически в процессе его работы выполнять пинг, чтобы 65 | «Битрикс» не считал его зависшим. Например: 66 | 67 | ```php 68 | public function execute($param1) 69 | { 70 | // Начало выполнения тяжёлых операций 71 | 72 | $this->pingAgent(20, ['execute' => [$param1]]); 73 | 74 | // Завершение выполнения 75 | } 76 | ``` -------------------------------------------------------------------------------- /docs/ru/application.md: -------------------------------------------------------------------------------- 1 | # Консольное приложение 2 | 3 | Рано или поздно практически на любом сайте встаёт необходимость выполнения скриптов в консоли, будь то воркеры, 4 | выполняющие в фоне ресурсоёмкие операции, или инструментарий по обслуживанию сайта. Кроме того, внедрение непрерывной 5 | интеграции также требует автоматизации настройки и обновления приложения. Приступая к созданию очередного консольного 6 | скрипта конечно же хочется свести к минимуму написание кода и уделить больше внимания основной логике. Решить все 7 | эти задачи вам поможет Console Jedi. 8 | 9 | Console Jedi — это, в первую очередь, консольное приложение для «Битрикса», позволяющее в CLI управлять системой 10 | и выполнять консольные команды. «Джедай» построен на базе [Symfony Console](https://github.com/symfony/console) — 11 | одного из самых популярных консольных приложений, — что позволяет вам без труда подключать сторонние консольные команды 12 | (например, PHP CPD, Phinx) и внедрять свои. 13 | 14 | Фактически, библиотека является прослойкой между пользователем и Symfony Console, которая позволяет управлять 15 | «Битриксом». «Джедай» пытается запустить «Битрикс» и, если это удалось, выводит консольные команды, которые не могут 16 | работать без него. 17 | 18 | Под фразой «запустить „Битрикс“» имеется в виду полная инициализация системы, т. к. без подключения к БД «Битрикс» 19 | полноценно работать не может. В случае, если «Битрикс» по каким-то причинам не поднялся, Console Jedi всё равно 20 | запустится, но будут доступны только те команды, которые не требуют наличия «Битрикса». Это удобно, например, для 21 | разворачивания проекта с чистого листа. -------------------------------------------------------------------------------- /docs/ru/cache.md: -------------------------------------------------------------------------------- 1 | # Кеширование 2 | 3 | Так или иначе иногда необходимо сбрасывать кеши. Чаще всего, это требуется во время разработки или тестирования приложения. 4 | В некоторых случаях это необходимо делать после деплоя приложения на боевые сервера. Однако, в последнем случае главное не 5 | злоупотреблять этой возможностью: если сайт не может обновляться без сброса кешей, где-то в вашей архитектуре затаилась ошибка. 6 | 7 | Удалением кешей занимается консольная команда `cache:clear`: 8 | ```bash 9 | cache:clear # Удалить весь кеш 10 | cache:clear --dir # Удалить кеш только из директории относительно /bitrix/cache/ 11 | cache:clear --tag # Удалить кеш по тегу 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/ru/ci-env-init-command.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/notamedia/console-jedi/c2af9676e6a068f702b9f587befea8f533d3cffa/docs/ru/ci-env-init-command.jpg -------------------------------------------------------------------------------- /docs/ru/ci.md: -------------------------------------------------------------------------------- 1 | # Continuous Integration 2 | 3 | С помощью Console Jedi сочетание слов «Битрикс» и CI становится реальностью: отныне проект можно развернуть и 4 | настроить одной консольной командой, как в песочнице у разработчика, так и на боевой площадке. 5 | 6 | 1. [Подключите](install.md) к своему проекту Console Jedi. 7 | 2. Настройки «Битрикса», различающиеся в зависимости от окружения (dev, prod), устанавливайте через 8 | [настройки окружений](environment.md). 9 | 10 | Тогда, после выкладки свежего кода на боевую площадку, будет достаточно установить пакеты через Composer и 11 | применить «боевые» настройки «Битрикса»: 12 | 13 | ```bash 14 | composer install --no-dev --prefer-dist --no-interaction 15 | ./vendor/bin/jedi env:init prod --no-interaction 16 | ``` 17 | 18 | Консольная команда `env:init`, в отличие от [остальных команд](command.md), имеет особый алгоритм выполнения, 19 | заслуживающий отдельного объяснения: 20 | 21 | ![env:init](ci-env-init-command.jpg) 22 | -------------------------------------------------------------------------------- /docs/ru/command.md: -------------------------------------------------------------------------------- 1 | # Консольные команды 2 | 3 | Console Jedi из коробки содержит ряд команд, упрощающих администрирование и поддержку сайта на «Битриксе». Узнать 4 | полный список вы можете вызвав консольное приложение: 5 | 6 | ```bash 7 | ./vendor/bin/jedi 8 | ``` 9 | 10 | Подробную информацию о команде можно запросить через опцию `--help`, например: 11 | 12 | ```bash 13 | ./vendor/bin/jedi cache:clear --help 14 | ``` 15 | 16 | ## Создание команды 17 | 18 | Написание собственных команд отличается от 19 | [написания команд для Symfony Console](http://symfony.com/doc/current/components/console/introduction.html) только 20 | классом-родителем: 21 | 22 | * `\Notamedia\ConsoleJedi\Application\Command\Command` — команда может работать при отсутствии ядра «Битрикса», 23 | * `\Notamedia\ConsoleJedi\Application\Command\BitrixCommand` — для работы команды обязательно должно быть 24 | инициализировано ядро «Битрикса». 25 | 26 | Во имя модульности проекта, свои консольные команды нужно размещать в модулях «Битрикса». Для этого в файле 27 | `vendor.module/cli.php` должны быть описаны консольные команды модуля: 28 | 29 | ```php 30 | [ 34 | new \Vendor\Module\Command\FirstCommand() 35 | ] 36 | ]; 37 | ``` 38 | 39 | Во время запуска Console Jedi автоматически загрузит команды всех установленных модулей. Кроме того, вы можете 40 | зарегистрировать дополнительные команды через настройки в `.jedi.php` (файл располагается в корне проекта): 41 | 42 | ```php 43 | [ 47 | new FirstCommand() 48 | ] 49 | ]; 50 | ``` 51 | 52 | ## Полезные материалы 53 | 54 | * [Документация по Symfony Console](http://symfony.com/doc/current/components/console/introduction.html). -------------------------------------------------------------------------------- /docs/ru/environment.md: -------------------------------------------------------------------------------- 1 | # Настройки окружений 2 | 3 | Приложение может работать в разных окружениях: начиная с боевой площадки, заканчивая песочницей разработчика, иногда 4 | имеющей некоторые отличия или ограничения. Разные окружения требуют разных настроек и наборов некоторых файлов. Console Jedi 5 | читает из каталога `/environments` список окружений и с помощью консольных команд умеет их применять. 6 | 7 | ## Хранилище окружений 8 | 9 | Каталог с настройками окружений `/environments` должен находится в корне проекта. Каталог состоит из: 10 | 11 | ``` 12 | /dev // Каталог с файлами dev-окружения. 13 | config.php // Конфигурация dev-окружения. 14 | /prod // Каталог с файлами prod-окружения. 15 | config.php // Конфигурация prod-окружения. 16 | index.php // Список существующих окружений. 17 | ``` 18 | 19 | По умолчанию в Console Jedi присутствует две заготовки под окружения: dev и prod. Вы можете удалить их, заменить или 20 | добавить другие. 21 | 22 | Применение настроек окружения производится через консольную команду `env:init `, вызов которой приводит к выполению 23 | инструкций, описанных в файле `/environments//config.php` и копированию всех остальных файлов, найденных в 24 | `/environments/`. 25 | 26 | Скрипт `/environments//config.php` должен возвращать массив с инструкциями по настройке окружения. Все настройки 27 | являются необязательными и вовсе могут отсутствовать. Поддерживаемые настройки: 28 | 29 | Настройка | Значение | Описание 30 | --- | --- | --- 31 | settings | array | Массив конфигурации для файла `.settings.php`. 32 | licenseKey | string | Лицензионный ключ продукта. 33 | modules | array | Список модулей, которые необходимо зарегистрировать в системе. 34 | options | array | Настройки модулей. Ключи элементов массива являются названиями модулей. Внутри элемента указывается код параметра и его значение. 35 | cluster | array | Настройки модуля «Веб-кластер». 36 | cluster[memcache] | array | Список мемкешей, которые должен использовать модуль «Веб-кластер». 37 | 38 | 39 | Пример: 40 | ```php 41 | [ 45 | 'connections' => [ 46 | 'default' => [ 47 | 'host' => 'host', 48 | 'database' => 'db', 49 | 'login' => 'login', 50 | 'password' => 'pass', 51 | 'className' => '\\Bitrix\\Main\\DB\\MysqlConnection', 52 | 'options' => 2, 53 | ] 54 | ] 55 | ], 56 | 'licenseKey' => 'NFR-123-456-789', 57 | 'modules' => [ 58 | 'iblock', 59 | 'notamedia.i18n' 60 | ], 61 | 'options' => [ 62 | 'main' => [ 63 | 'server_name' => 'star-wars.dev', 64 | 'email_from' => 'admin@star-wars.dev' 65 | ] 66 | ], 67 | 'cluster' => [ 68 | 'memcache' => [ 69 | [ 70 | 'GROUP_ID' => 1, 71 | 'HOST' => 'host', 72 | 'PORT' => 'port', 73 | 'WEIGHT' => 'weight', 74 | 'STATUS' => 'status', 75 | ], 76 | [ 77 | 'GROUP_ID' => 1, 78 | 'HOST' => 'host', 79 | 'PORT' => 'port', 80 | 'WEIGHT' => 'weight', 81 | 'STATUS' => 'status', 82 | ] 83 | ] 84 | ] 85 | ]; 86 | ``` 87 | 88 | Попробуем на практике. Создадим окружение с кодом `r2`. 89 | 90 | 1. Создайте каталог `/environments/r2`. 91 | 1. Предположим, что в проекте в окружении r2 нужен файл `/directory/file.txt`. Поэтому поместим его в настройки 92 | окружения: `/environments/r2/directory/file.txt`. 93 | 1. Создайте файл с инструкциями по настройке окружения: `/environments/r2/config.php`. Данный скрипт должен 94 | возвращать массив (можно и пустой). 95 | 1. Окружение готово, осталось всего лишь зарегистрировать его. В файле `/environments/index.php` добавим новый элемент 96 | массива: 97 | 98 | ``` 99 | 'r2' => [ 100 | 'name' => 'R2-D2', // Полное название окружения, отображается в интерфейсе Console Jedi. 101 | 'path' => 'r2' // Каталог с окружением, относительно текущего файла. 102 | ], 103 | ``` 104 | 105 | Запустите команду `env:init r2`. Будет скопирован файл из `/environments/r2/directory/file.txt` в `/directory/file.txt` и 106 | выполнены указания, описанные в файле `config.php`. 107 | 108 | ## Рекомендации 109 | 110 | Настройки в «Битриксе» задаются двумя способами: через [`.settings*.php`](https://dev.1c-bitrix.ru/learning/course/?COURSE_ID=43&LESSON_ID=2795) 111 | и с помощью настроек модулей. Первым способом предпочтительнее задавать неизменяемые параметры, причём рекомендуется 112 | работать с файлом `.settings_extra.php`, тем самым переназначив настройки, указанные в `.settings.php`. Разместите в 113 | каталоге с окружением файл `///.settings_extra.php`, возвращающий массив, и опишите в нём необходимые 114 | данному окружению настройки. 115 | 116 | Несмотря на то, что Console Jedi позволяет управлять настройками модулей и их регистрацией, использовать эту возможность 117 | нужно крайне осторожно, потому что установка и удаление модулей, изменение их параметров — это операции изменения БД, 118 | которые должны производиться только через миграции. К примеру, вы просто не сможете удалить модуль из системы, используя 119 | настройки окружений. Установка модулей через настройки окружений не годна для продакшена, но хороша для дева и теста: 120 | например, так можно установить модуль для дебага или автотестов. -------------------------------------------------------------------------------- /docs/ru/install.md: -------------------------------------------------------------------------------- 1 | # Установка 2 | 3 | * Установите Composer, если ещё не сделали это. 4 | * Добавьте Console Jedi в зависимость Composer: 5 | ```bash 6 | composer require notamedia/console-jedi 7 | ``` 8 | * Выполните команду инициализации Console Jedi: 9 | ```bash 10 | ./vendor/bin/jedi init 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/ru/module.md: -------------------------------------------------------------------------------- 1 | # Работа с модулями 2 | 3 | Команды неймспейса `module` предназначена для управления модулями *1С-Битрикс*: установки, удаления, а также загрузки и установки новых версий из [Marketplace](http://marketplace.1c-bitrix.ru). 4 | 5 | ## Важная информация 6 | 7 | > Не все модули поддерживают режим автоматической установки, возможны проблемы с некоторыми сторонними решениями. 8 | 9 | Существует два способа [установки модулей](https://dev.1c-bitrix.ru/learning/course/?COURSE_ID=43&LESSON_ID=3475): 10 | 11 | * **Классический**. Привычный интерактивный режим установки через административную часть. 12 | При этом вызываются методы `DoInstall` и `DoUninstall` объекта модуля. 13 | 14 | * **Автоматический**. Выполняется на этапе установки продукта «1C-Битрикс: Управление сайтом». В **Console Jedi** используется этот способ. 15 | При этом последовательно вызываются методы объекта модуля `InstallDB`, `InstallEvents`, `InstallFiles`. 16 | 17 | В документации продукта 1С-Битрикс не описана возможность такой автоматической установки, но на практике любой модуль, который положили в `/bitrix/modules/` до установки ядра, будет установлен вместе с ядром. Автоматическая установка модуля также используется при [загрузке решения из Marketplace на этапе установки](https://dev.1c-bitrix.ru/learning/course/?COURSE_ID=35&LESSON_ID=3181). 18 | 19 | Разработчики сторонних модулей часто реализуют только методы `DoInstall` и `DoUninstall`, тем самым, делая невозможной установку в автоматическом режиме. 20 | 21 | > В своих модулях используйте `DoInstall` и `DoUninstall` только для вывода и обработки **пользовательского интерфейса**, а действия по установке/удалению модуля реализуйте в методах `InstallDB`, `InstallEvents`, `InstallFiles`. 22 | > 23 | > Корректную реализацию смотрите в [примере класса модуля](https://dev.1c-bitrix.ru/learning/course/?COURSE_ID=43&LESSON_ID=3223). 24 | 25 | ## Загрузка модуля (`module:load`) 26 | 27 | Загружает модуль из Marketplace (если не загружен), устанавливает все обновления модуля и устанавливает его. 28 | 29 | ``` 30 | module:load [-ct|--confirm-thirdparty] [-nu|--no-update] [-ni|--no-register] [-b|--beta] [--] 31 | ``` 32 | 33 | Опции | | Описание 34 | ---|----|--- 35 | -ct | --confirm-thirdparty | Пропустить предупреждение об установке сторонних модулей 36 | -nu | --no-update | Не устанавливать обновления 37 | -ni | --no-register | Не устанавливать модуль (только загрузить) 38 | -b | --beta | Включить загрузку и установку бета-версий модуля 39 | \ | | Код модуля (vendor.module) 40 | 41 | ## Установка модуля (`module:register`) 42 | 43 | Устанавливает существующий модуль. 44 | 45 | ``` 46 | module:register [-ct|--confirm-thirdparty] [--] 47 | ``` 48 | 49 | Опции | | Описание 50 | ---|----|--- 51 | -ct | --confirm-thirdparty | Пропустить предупреждение об установке сторонних модулей 52 | \ | | Код модуля (vendor.module) 53 | 54 | ## Удаление модуля (`module:unregister` и `module:remove`) 55 | 56 | Удаляет модуль из системы. 57 | 58 | `module:remove` дополнительно удаляет файлы модуля из `/bitrix/modules/` или `/local/modules/`. 59 | 60 | ``` 61 | module:unregister [-ct|--confirm-thirdparty] [--] 62 | module:remove [-ct|--confirm-thirdparty] [--] 63 | ``` 64 | 65 | Опции | | Описание 66 | ---|----|--- 67 | -ct | --confirm-thirdparty | Пропустить предупреждение об установке сторонних модулей 68 | \ | | Код модуля (vendor.module) 69 | 70 | ## Обновление модуля (`module:update`) 71 | 72 | Устанавливает обновления указанного модуля из *Marketplace*. 73 | 74 | > На данный момент установка обновлений модулей ядра не поддерживается. 75 | 76 | ``` 77 | module:update [-ct|--confirm-thirdparty] [-b|--beta] [--] 78 | ``` 79 | 80 | Опции | | Описание 81 | ---|----|--- 82 | -ct | --confirm-thirdparty | Пропустить предупреждение об установке сторонних модулей 83 | -b | --beta | Включить загрузку и установку бета-версий модуля 84 | \ | | Код модуля (vendor.module) 85 | -------------------------------------------------------------------------------- /docs/ru/search.md: -------------------------------------------------------------------------------- 1 | # Команды модуля поиска (search) 2 | 3 | Будут полезны при внесении изменений в обработчики или настройки модуля. 4 | 5 | ## Генерация поискового индекса (`search:reindex`) 6 | 7 | Производит [переиндексацию сайта](https://dev.1c-bitrix.ru/learning/course/?COURSE_ID=35&LESSON_ID=2048) аналогично форме на странице *Настройки > Поиск > Переиндексация*. 8 | 9 | ``` 10 | search:reindex [-f|--full] 11 | ``` 12 | 13 | Опции | | Описание 14 | ------|--------|--- 15 | `-f` | `--full` | Очищает поисковый индекс перед индексацией (без этой опции действие будет аналогично стандартной переиндексации с галочкой **Переиндексировать только измененные**) 16 | 17 | -------------------------------------------------------------------------------- /src/Agent/Agent.php: -------------------------------------------------------------------------------- 1 | run()`. Your agents should be registered in the same format: 14 | * `\Vendor\Packeage\ClassName::agent()->run();`. All arguments from this method will be duplicated to the 15 | * object constructor: 16 | * `agent($arg1, …, $arg2)` → `__construct($arg1, …, $arg2)`. 17 | * 2. Create an object of agent class. 18 | * 3. Call `init()` method. It is needed for some initial operations, for example: loading required modules. 19 | * 4. Call `execute()` method. This will execute main agent's logic. 20 | * 21 | * @author Nik Samokhvalov 22 | */ 23 | abstract class Agent 24 | { 25 | use AgentTrait; 26 | 27 | /** 28 | * Runs the Agent. 29 | * 30 | * Notice, that overriding agent's initialisation and body, should be done though `init` and `execute` methods, 31 | * not here. 32 | * 33 | * @see Agent::init() 34 | * @see Agent::execute() 35 | */ 36 | public function run() 37 | { 38 | $this->init(); 39 | 40 | return $this->execute(); 41 | } 42 | 43 | /** 44 | * Initialization of the agent. 45 | */ 46 | protected function init() 47 | { 48 | } 49 | 50 | /** 51 | * Agent execution. 52 | * 53 | * @return string Agent name if need again add his to queue. Use `$this->getAgentName()` for get name of agent. 54 | */ 55 | protected function execute() 56 | { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Agent/AgentHelper.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class AgentHelper 17 | { 18 | /** 19 | * Creates and returns agent name by class name and parameters. 20 | * Use to return this name from the executed method of agent. 21 | * 22 | * @param string $className Agent class name. 23 | * @param array $args Arguments for `__constructor` of agent class. 24 | * @param array $callChain 25 | * 26 | * @return string 27 | * @throws ArgumentTypeException 28 | */ 29 | public static function createName($className, array $args = [], array $callChain = []) 30 | { 31 | $chain = ''; 32 | 33 | if (!empty($callChain)) { 34 | foreach ($callChain as $method => $methodArgs) { 35 | if (!is_array($methodArgs)) { 36 | throw new ArgumentTypeException('callChain', 'array'); 37 | } 38 | 39 | $chain .= '->' . $method . '(' . static::convertArgsToString($methodArgs) . ')'; 40 | } 41 | } 42 | 43 | return '\\' . $className . '::agent(' . static::convertArgsToString($args) . ')' . $chain . ';'; 44 | } 45 | 46 | protected static function convertArgsToString(array $args) 47 | { 48 | $args = json_encode($args, JSON_UNESCAPED_SLASHES); 49 | $args = str_replace(',', ', ', $args); 50 | $args = substr($args, 1); 51 | $args = substr($args, 0, -1); 52 | 53 | return $args; 54 | } 55 | } -------------------------------------------------------------------------------- /src/Agent/AgentTask.php: -------------------------------------------------------------------------------- 1 | setClass(TestAgent::class) 21 | * ->setConstructorArgs(['arg1', true]) 22 | * ->setCallChain([ 23 | * ['execute' => [100500]] 24 | * ]), 25 | * ->setModule('vendor.module') 26 | * ->create(); 27 | * ``` 28 | * The result: will be the registered agent `\Vendor\Module\TestAgent::agent('arg1', true)->execute(100500);`. 29 | * 30 | * @author Nik Samokhvalov 31 | */ 32 | class AgentTask 33 | { 34 | protected $class; 35 | protected $constructorArgs = []; 36 | protected $callChain; 37 | protected $module; 38 | protected $interval; 39 | protected $periodically = false; 40 | protected $active = true; 41 | protected $executionTime; 42 | protected $sort; 43 | protected $userId; 44 | 45 | /** 46 | * Builder for create new task to the queue of agents. 47 | * 48 | * @return static 49 | */ 50 | public static function build() 51 | { 52 | return new static; 53 | } 54 | 55 | /** 56 | * Sets agent class name. 57 | * 58 | * @param string $className 59 | * 60 | * @return $this 61 | */ 62 | public function setClass($className) 63 | { 64 | $this->class = $className; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Sets the arguments for `__constructor` of agent class. 71 | * 72 | * @param array $args 73 | * 74 | * @return $this 75 | */ 76 | public function setConstructorArgs(array $args) 77 | { 78 | $this->constructorArgs = $args; 79 | 80 | return $this; 81 | } 82 | 83 | /** 84 | * Sets the chain methods with arguments for add them to agent name for execution. 85 | * 86 | * @param array $callChain 87 | * 88 | * @return $this 89 | */ 90 | public function setCallChain(array $callChain) 91 | { 92 | $this->callChain = $callChain; 93 | 94 | return $this; 95 | } 96 | 97 | /** 98 | * Sets the name of the module to which the agent belongs. 99 | * 100 | * @param string $moduleName 101 | * 102 | * @return $this 103 | */ 104 | public function setModule($moduleName) 105 | { 106 | $this->module = $moduleName; 107 | 108 | return $this; 109 | } 110 | 111 | /** 112 | * Sets the time interval between execution. 113 | * 114 | * @param int $seconds 115 | * 116 | * @return $this 117 | */ 118 | public function setInterval($seconds) 119 | { 120 | $this->interval = (int)$seconds; 121 | 122 | return $this; 123 | } 124 | 125 | /** 126 | * Sets the periodically mode of agent. 127 | * 128 | * @param bool $periodically 129 | * 130 | * @return $this 131 | */ 132 | public function setPeriodically($periodically) 133 | { 134 | $this->periodically = (bool)$periodically; 135 | 136 | return $this; 137 | } 138 | 139 | /** 140 | * Sets the activity of agent. 141 | * 142 | * @param bool $active 143 | * 144 | * @return $this 145 | */ 146 | public function setActive($active) 147 | { 148 | $this->active = (bool)$active; 149 | 150 | return $this; 151 | } 152 | 153 | /** 154 | * Sets first execution time. 155 | * 156 | * @param DateTime $time 157 | * 158 | * @return $this 159 | */ 160 | public function setExecutionTime(DateTime $time) 161 | { 162 | $this->executionTime = $time; 163 | 164 | return $this; 165 | } 166 | 167 | /** 168 | * Sets sorting. 169 | * 170 | * @param int $sort 171 | * 172 | * @return $this 173 | */ 174 | public function setSort($sort) 175 | { 176 | $this->sort = (int)$sort; 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * Sets user ID on whose behalf the agent is executed. 183 | * 184 | * @param int $userId User ID. 185 | * 186 | * @return $this 187 | */ 188 | public function setUserId($userId) 189 | { 190 | $this->userId = (int)$userId; 191 | 192 | return $this; 193 | } 194 | 195 | /** 196 | * Convertation property for creation agent in queue through the old and dirty Bitrix API. 197 | */ 198 | protected function convertation() 199 | { 200 | if ($this->executionTime instanceof DateTime) { 201 | $this->executionTime = $this->executionTime->toString(); 202 | } elseif ($this->executionTime === null) { 203 | $time = new DateTime(); 204 | $this->executionTime = $time->toString(); 205 | } 206 | 207 | foreach (['periodically', 'active'] as $property) { 208 | if ($this->$property === true) { 209 | $this->$property = 'Y'; 210 | } else { 211 | $this->$property = 'N'; 212 | } 213 | } 214 | } 215 | 216 | /** 217 | * Create agent in Bitrix queue. 218 | * 219 | * @param bool $checkExist Return false and set `CAdminException`, if agent already exist. 220 | * 221 | * @return bool|int ID of agent or false if `$checkExist` is true and agent already exist. 222 | */ 223 | public function create($checkExist = false) 224 | { 225 | $this->convertation(); 226 | 227 | $model = new \CAgent; 228 | 229 | return $model->AddAgent( 230 | AgentHelper::createName($this->class, $this->constructorArgs, $this->callChain), 231 | $this->module, 232 | $this->periodically, 233 | $this->interval, 234 | null, 235 | $this->active, 236 | $this->executionTime, 237 | $this->sort, 238 | $this->userId, 239 | $checkExist 240 | ); 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/Agent/AgentTrait.php: -------------------------------------------------------------------------------- 1 | %method%()`. Your agents should be registered through 16 | * `\Notamedia\ConsoleJedi\Agent\AgentTask` in the same format: `\Vendor\Package\ClassName::agent()->%method%();`. 17 | * All arguments from this method will be duplicated to the object constructor: 18 | * `agent($arg1, …, $arg2)` → `__construct($arg1, …, $arg2)`. 19 | * 2. Create an object of agent class. 20 | * 3. Call execution method in agent class. 21 | * 22 | * @author Nik Samokhvalov 23 | */ 24 | trait AgentTrait 25 | { 26 | /** 27 | * @var array Arguments for `__constructor`. 28 | */ 29 | protected static $constructorArgs; 30 | /** 31 | * @var bool 32 | */ 33 | protected static $agentMode = false; 34 | 35 | /** 36 | * Agent constructor. 37 | * 38 | * All arguments from `agent()` method should be duplicated in the constructor, for example: 39 | * ``` 40 | * agent($arg1, …, $arg2)` → `__construct($arg1, …, $arg2) 41 | * ``` 42 | */ 43 | public function __construct() 44 | { 45 | } 46 | 47 | /** 48 | * Factory method for create object of agent class. 49 | * 50 | * Bitrix calls this method to run agent. Your agents should be registered through 51 | * `\Notamedia\ConsoleJedi\Agent\AgentTask`. All arguments from this method should 52 | * be duplicated in the object constructor: 53 | * 54 | * `agent($arg1, …, $arg2)` → `__construct($arg1, …, $arg2)`. 55 | * 56 | * @return static 57 | * 58 | * @see AgentTask 59 | */ 60 | public static function agent() 61 | { 62 | static::$constructorArgs = func_get_args(); 63 | static::$agentMode = true; 64 | 65 | $reflection = new \ReflectionClass(get_called_class()); 66 | 67 | return $reflection->newInstanceArgs(static::$constructorArgs); 68 | } 69 | 70 | /** 71 | * Ping from the agent to inform that it still works correctly. Use this method if your agent 72 | * works more 10 minutes, otherwise Bitrix will be consider your agent as non-working. 73 | * 74 | * Usage: 75 | * ```php 76 | * public function executeAgent($param1, $param2) 77 | * { 78 | * // start a heavy (big) cycle 79 | * 80 | * $this->pingAgent(20, ['executeAgent' => [$param1, $param2]]); 81 | * 82 | * // end of cycle 83 | * } 84 | * ``` 85 | * 86 | * @param int $interval The time in minutes after which the agent will be considered non-working. 87 | * @param array $callChain Array with the call any methods from Agent class. 88 | */ 89 | protected function pingAgent($interval, array $callChain) 90 | { 91 | if (!$this->isAgentMode()) { 92 | return; 93 | } 94 | 95 | $name = $this->getAgentName($callChain); 96 | $model = new \CAgent(); 97 | 98 | $rsAgent = $model->GetList([], ['NAME' => $name]); 99 | 100 | if ($agent = $rsAgent->Fetch()) { 101 | $dateCheck = DateTime::createFromTimestamp(time() + $interval * 60); 102 | 103 | $pingResult = $model->Update($agent['ID'], ['DATE_CHECK' => $dateCheck->toString()]); 104 | 105 | if (!$pingResult) { 106 | // @todo warning 107 | } 108 | } else { 109 | // @todo warning 110 | } 111 | } 112 | 113 | /** 114 | * Gets agent name. Use to return this name from the executed method of agent. 115 | * 116 | * Usage: 117 | * ```php 118 | * public function executeAgent($param1, $param2) 119 | * { 120 | * // main logic 121 | * 122 | * return $this->getAgentName(['executeAgent' => [$param1, $param2]]); 123 | * } 124 | * ``` 125 | * 126 | * @param array $callChain Array with the call any methods from Agent class. 127 | * 128 | * @return string 129 | */ 130 | public function getAgentName(array $callChain) 131 | { 132 | return AgentHelper::createName(get_called_class(), static::$constructorArgs, $callChain); 133 | } 134 | 135 | /** 136 | * Checks that object running as agent. Object is considered an agent 137 | * if it is created using the static method `agent()`. 138 | * 139 | * @return bool 140 | */ 141 | public function isAgentMode() 142 | { 143 | return static::$agentMode; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Agent/Command/ExecuteCommand.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class ExecuteCommand extends BitrixCommand 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function configure() 24 | { 25 | parent::configure(); 26 | 27 | $this->setName('agent:execute') 28 | ->setDescription('Execution of tasks from agents queue'); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function execute(InputInterface $input, OutputInterface $output) 35 | { 36 | @set_time_limit(0); 37 | @ignore_user_abort(true); 38 | define('CHK_EVENT', true); 39 | 40 | $agentManager = new \CAgent(); 41 | $agentManager->CheckAgents(); 42 | 43 | define('BX_CRONTAB_SUPPORT', true); 44 | define('BX_CRONTAB', true); 45 | 46 | $eventManager = new \CEvent(); 47 | $eventManager->CheckEvents(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Agent/Command/OnCronCommand.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class OnCronCommand extends BitrixCommand 20 | { 21 | /** 22 | * {@inheritdoc} 23 | */ 24 | protected function configure() 25 | { 26 | parent::configure(); 27 | 28 | $this->setName('agent:on-cron') 29 | ->setDescription('Installation configurations for run Agents on cron'); 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | protected function execute(InputInterface $input, OutputInterface $output) 36 | { 37 | Option::set('main', 'agents_use_crontab', 'N'); 38 | Option::set('main', 'check_agents', 'N'); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Application/Application.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class Application extends \Symfony\Component\Console\Application 32 | { 33 | /** 34 | * Version of the Console Jedi application. 35 | */ 36 | const VERSION = '1.0.0'; 37 | /** 38 | * Default name of configuration file. 39 | */ 40 | const CONFIG_DEFAULT_FILE = './.jedi.php'; 41 | /** 42 | * Bitrix is unavailable. 43 | */ 44 | const BITRIX_STATUS_UNAVAILABLE = 500; 45 | /** 46 | * Bitrix is available, but not have connection to DB. 47 | */ 48 | const BITRIX_STATUS_NO_DB_CONNECTION = 100; 49 | /** 50 | * Bitrix is available. 51 | */ 52 | const BITRIX_STATUS_COMPLETE = 0; 53 | /** 54 | * @var int Status of Bitrix kernel. Value of constant `Application::BITRIX_STATUS_*`. 55 | */ 56 | protected $bitrixStatus = Application::BITRIX_STATUS_UNAVAILABLE; 57 | /** 58 | * @var null|string 59 | */ 60 | private $documentRoot = null; 61 | /** 62 | * @var null|array 63 | */ 64 | private $configuration = null; 65 | 66 | /** 67 | * {@inheritdoc} 68 | */ 69 | public function __construct($name = 'Console Jedi', $version = self::VERSION) 70 | { 71 | parent::__construct($name, static::VERSION); 72 | } 73 | 74 | /** 75 | * {@inheritdoc} 76 | */ 77 | public function doRun(InputInterface $input, OutputInterface $output) 78 | { 79 | if ($this->getConfiguration() === null) { 80 | $this->loadConfiguration(); 81 | } 82 | 83 | if (!in_array($this->getCommandName($input), ['environment:init', 'env:init'])) { 84 | $this->initializeBitrix(); 85 | } 86 | 87 | if ($this->getConfiguration()) { 88 | foreach ($this->getBitrixCommands() as $bitrixCommand) { 89 | $this->add($bitrixCommand); 90 | } 91 | 92 | foreach ($this->getConfiguration()['commands'] as $command) { 93 | $this->add($command); 94 | } 95 | } 96 | 97 | if ($this->isBitrixLoaded() && $this->getConfiguration()['useModules'] === true) { 98 | foreach ($this->getModulesCommands() as $moduleCommand) { 99 | $this->add($moduleCommand); 100 | } 101 | } 102 | 103 | $exitCode = parent::doRun($input, $output); 104 | 105 | if ($this->getConfiguration() === null) { 106 | $output->writeln(PHP_EOL . 'No configuration loaded. ' 107 | . 'Please run init command first'); 108 | } else { 109 | switch ($this->getBitrixStatus()) { 110 | case static::BITRIX_STATUS_UNAVAILABLE: 111 | $output->writeln(PHP_EOL . sprintf('No Bitrix kernel found in %s. ' 112 | . 'Please run env:init command to configure', $this->getDocumentRoot())); 113 | break; 114 | 115 | case static::BITRIX_STATUS_NO_DB_CONNECTION: 116 | $output->writeln(PHP_EOL . 'Bitrix database connection is unavailable.'); 117 | break; 118 | 119 | case static::BITRIX_STATUS_COMPLETE: 120 | if ($this->getCommandName($input) === null) { 121 | $output->writeln(PHP_EOL . sprintf('Using Bitrix kernel v%s.', SM_VERSION), 122 | OutputInterface::VERBOSITY_VERY_VERBOSE); 123 | } 124 | break; 125 | } 126 | } 127 | 128 | return $exitCode; 129 | } 130 | 131 | /** 132 | * {@inheritdoc} 133 | */ 134 | protected function getDefaultCommands() 135 | { 136 | $commands = parent::getDefaultCommands(); 137 | $commands[] = new \Notamedia\ConsoleJedi\Application\Command\InitCommand(); 138 | 139 | return $commands; 140 | } 141 | 142 | /** 143 | * Gets Bitrix console commands from this package. 144 | * 145 | * @return Command[] 146 | */ 147 | protected function getBitrixCommands() 148 | { 149 | return array_merge( 150 | [ 151 | new OnCronCommand(), 152 | new ExecuteCommand(), 153 | new ClearCommand(), 154 | new InitCommand(), 155 | new ReIndexCommand(), 156 | new ExportCommand(), 157 | new ImportCommand(), 158 | new ReIndexCommand(), 159 | ], 160 | Module\ModuleCommand::getCommands() 161 | ); 162 | } 163 | 164 | /** 165 | * Gets console commands from modules. 166 | * 167 | * @return Command[] 168 | * 169 | * @throws \Bitrix\Main\LoaderException 170 | */ 171 | protected function getModulesCommands() 172 | { 173 | $commands = []; 174 | 175 | foreach (ModuleManager::getInstalledModules() as $module) { 176 | $cliFile = getLocalPath('modules/' . $module['ID'] . '/cli.php'); 177 | 178 | if ($cliFile === false) { 179 | continue; 180 | } elseif (!Loader::includeModule($module['ID'])) { 181 | continue; 182 | } 183 | 184 | $config = include_once $this->getDocumentRoot() . $cliFile; 185 | 186 | if (isset($config['commands']) && is_array($config['commands'])) { 187 | $commands = array_merge($commands, $config['commands']); 188 | } 189 | } 190 | 191 | return $commands; 192 | } 193 | 194 | /** 195 | * Loading application configuration. 196 | * 197 | * @param string $path Path to configuration file. 198 | * 199 | * @return bool 200 | * 201 | * @throws ConfigurationException 202 | */ 203 | public function loadConfiguration($path = self::CONFIG_DEFAULT_FILE) 204 | { 205 | if (!is_file($path)) { 206 | return false; 207 | } 208 | 209 | $this->configuration = include $path; 210 | 211 | if (!is_array($this->configuration)) { 212 | throw new ConfigurationException('Configuration file ' . $path . ' must return an array'); 213 | } 214 | 215 | $filesystem = new Filesystem(); 216 | 217 | if ($filesystem->isAbsolutePath($this->configuration['web-dir'])) { 218 | $this->setDocumentRoot($this->configuration['web-dir']); 219 | } else { 220 | $this->setDocumentRoot($this->getRoot() . '/' . $this->configuration['web-dir']); 221 | } 222 | 223 | if (!is_dir($_SERVER['DOCUMENT_ROOT'])) { 224 | return false; 225 | } 226 | 227 | return true; 228 | } 229 | 230 | /** 231 | * Gets application configuration. 232 | * 233 | * @return null|array 234 | */ 235 | public function getConfiguration() 236 | { 237 | return $this->configuration; 238 | } 239 | 240 | /** 241 | * Initialize kernel of Bitrix. 242 | * 243 | * @return int The status of readiness kernel. 244 | */ 245 | public function initializeBitrix() 246 | { 247 | if ($this->bitrixStatus === static::BITRIX_STATUS_COMPLETE) { 248 | return static::BITRIX_STATUS_COMPLETE; 249 | } elseif (!$this->checkBitrix()) { 250 | return static::BITRIX_STATUS_UNAVAILABLE; 251 | } 252 | 253 | define('NO_KEEP_STATISTIC', true); 254 | define('NOT_CHECK_PERMISSIONS', true); 255 | 256 | try { 257 | /** 258 | * Declare global legacy variables 259 | * 260 | * Including kernel here makes them local by default but some modules depend on them in installation class 261 | */ 262 | global 263 | /** @noinspection PhpUnusedLocalVariableInspection */ 264 | $DB, $DBType, $DBHost, $DBLogin, $DBPassword, $DBName, $DBDebug, $DBDebugToFile, $APPLICATION, $USER, $DBSQLServerType; 265 | 266 | require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php'; 267 | 268 | if (defined('B_PROLOG_INCLUDED') && B_PROLOG_INCLUDED === true) { 269 | $this->bitrixStatus = static::BITRIX_STATUS_COMPLETE; 270 | } 271 | } catch (ConnectionException $e) { 272 | $this->bitrixStatus = static::BITRIX_STATUS_NO_DB_CONNECTION; 273 | } 274 | 275 | return $this->bitrixStatus; 276 | } 277 | 278 | /** 279 | * Checks readiness of Bitrix for kernel initialize. 280 | * 281 | * @return bool 282 | */ 283 | public function checkBitrix() 284 | { 285 | if ( 286 | !is_file($_SERVER['DOCUMENT_ROOT'] . '/bitrix/.settings.php') 287 | && !is_file($_SERVER['DOCUMENT_ROOT'] . '/bitrix/.settings_extra.php') 288 | ) { 289 | return false; 290 | } 291 | 292 | return true; 293 | } 294 | 295 | /** 296 | * Gets Bitrix status. 297 | * 298 | * @return int Value of constant `Application::BITRIX_STATUS_*`. 299 | */ 300 | public function getBitrixStatus() 301 | { 302 | return $this->bitrixStatus; 303 | } 304 | 305 | /** 306 | * Checks that the Bitrix kernel is loaded. 307 | * 308 | * @return bool 309 | */ 310 | public function isBitrixLoaded() 311 | { 312 | return $this->bitrixStatus === static::BITRIX_STATUS_COMPLETE; 313 | } 314 | 315 | /** 316 | * Autoloader classes of the tests. 317 | * 318 | * Initializes Bitrix kernel, finds and connects files in directory `vendor.module/tests/` 319 | * by pattern `test.php` and loading modules of tests. 320 | * 321 | * @throws ConfigurationException 322 | */ 323 | public function autoloadTests() 324 | { 325 | if ($this->getConfiguration() === null) { 326 | $this->loadConfiguration(); 327 | } 328 | 329 | $this->initializeBitrix(); 330 | 331 | spl_autoload_register(function ($className) { 332 | $file = ltrim($className, "\\"); 333 | $file = strtr($file, Loader::ALPHA_UPPER, Loader::ALPHA_LOWER); 334 | $file = str_replace('\\', '/', $file); 335 | 336 | if (substr($file, -5) === 'table') { 337 | $file = substr($file, 0, -5); 338 | } 339 | 340 | $arFile = explode('/', $file); 341 | 342 | if (preg_match("#[^\\\\/a-zA-Z0-9_]#", $file)) { 343 | return false; 344 | } elseif ($arFile[0] === 'bitrix') { 345 | return false; 346 | } elseif ($arFile[2] !== 'tests') { 347 | return false; 348 | } 349 | 350 | $module = array_shift($arFile) . '.' . array_shift($arFile); 351 | 352 | if (!Loader::includeModule($module)) { 353 | return false; 354 | } 355 | 356 | $path = getLocalPath('/modules/' . $module . '/' . implode('/', $arFile) . '.php'); 357 | 358 | if ($path !== false) { 359 | include_once $this->getDocumentRoot() . $path; 360 | } 361 | }); 362 | } 363 | 364 | /** 365 | * Gets root directory from which are running Console Jedi. 366 | * 367 | * @return string 368 | */ 369 | public function getRoot() 370 | { 371 | return getcwd(); 372 | } 373 | 374 | /** 375 | * Sets path to the document root of site. 376 | * 377 | * @param string $dir Path to document root. 378 | */ 379 | public function setDocumentRoot($dir) 380 | { 381 | $_SERVER['DOCUMENT_ROOT'] = $this->documentRoot = $dir; 382 | } 383 | 384 | /** 385 | * Gets document root of site. 386 | * 387 | * @return null|string 388 | */ 389 | public function getDocumentRoot() 390 | { 391 | return $this->documentRoot; 392 | } 393 | } -------------------------------------------------------------------------------- /src/Application/CanRestartTrait.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | trait CanRestartTrait 18 | { 19 | /** 20 | * Executes another copy of console process to continue updates 21 | * 22 | * We may encounter problems when module update scripts (update.php or update_post.php) requires module files, 23 | * they are included only once and stay in most early version. 24 | * Bitrix update system always run update scripts in separate requests to web-server. 25 | * This ensures the same behavior as in original update system, updates always run on latest module version. 26 | * 27 | * @param InputInterface $input 28 | * @param OutputInterface $output 29 | * 30 | * @return int 31 | */ 32 | protected function restartScript(InputInterface $input, OutputInterface $output) 33 | { 34 | $proc = popen('php -f ' . join(' ', $GLOBALS['argv']) . ' 2>&1', 'r'); 35 | while (!feof($proc)) { 36 | $output->write(fread($proc, 4096)); 37 | } 38 | 39 | return pclose($proc); 40 | } 41 | } -------------------------------------------------------------------------------- /src/Application/Command/BitrixCommand.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class BitrixCommand extends Command 15 | { 16 | /** 17 | * {@inheritdoc} 18 | */ 19 | public function isEnabled() 20 | { 21 | if ($this->getApplication()->isBitrixLoaded()) { 22 | return true; 23 | } 24 | 25 | return false; 26 | } 27 | } -------------------------------------------------------------------------------- /src/Application/Command/Command.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class Command extends \Symfony\Component\Console\Command\Command 15 | { 16 | /** 17 | * @return \Notamedia\ConsoleJedi\Application\Application 18 | */ 19 | public function getApplication() 20 | { 21 | return parent::getApplication(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/Application/Command/InitCommand.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class InitCommand extends Command 23 | { 24 | const COMPLETED_LOGO = ' 25 | ____ 26 | _.\' : `._ 27 | .-.\'`. ; .\'`.-. 28 | __ / : ___\ ; /___ ; \ __ 29 | ,\'_ ""--.:__;".-.";: :".-.":__;.--"" _`, 30 | :\' `.t""--.. \'<@.`;_ \',@>` ..--""j.\' `; 31 | `:-.._J \'-.-\'L__ `-- \' L_..-;\' 32 | "-.__ ; .-" "-. : __.-" 33 | L \' /.------.\ \' J 34 | "-. "--" .-" 35 | __.l"-:_JL_;-";.__ 36 | .-j/\'.; ;"""" / .\'\"-. 37 | .\' /:`. "-.: .-" .\'; `. 38 | .-" / ; "-. "-..-" .-" : "-. 39 | .+"-. : : "-.__.-" ;-._ \ 40 | ; \ `.; ; : : "+. ; 41 | : ; ; ; : ; : \: 42 | : `."-; ; ; : ; ,/; 43 | ; -: ; : ; : .-"\' : 44 | :\ \ : ; : \.-" : 45 | ;`. \ ; : ;.\'_..-- / ; 46 | : "-. "-: ; :/." .\' : 47 | \ .-`.\ /t-"" ":-+. : 48 | `. .-" `l __/ /`. : ; ; \ ; 49 | \ .-" .-"-.-" .\' .\'j \ / ;/ 50 | \ / .-" /. .\'.\' ;_:\' ; 51 | :-""-.`./-.\' / `.___.\' 52 | \ `t ._ / bug :F_P: 53 | "-.t-._:\' 54 | 55 | Installation is completed. 56 | May the Force be with you. 57 | '; 58 | 59 | /** 60 | * @var string Path to directory with templates of the application files. 61 | */ 62 | protected $tmplDir; 63 | /** 64 | * @var string Default name of directory with environments settings. 65 | */ 66 | protected $envDir = 'environments'; 67 | /** 68 | * @var QuestionHelper $question 69 | */ 70 | protected $questionHelper; 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | protected function configure() 76 | { 77 | $this->setName('init') 78 | ->setDescription('Initialize the Console Jedi') 79 | ->addOption('force', 'f', InputOption::VALUE_NONE, 'Override an existing files'); 80 | } 81 | 82 | /** 83 | * {@inheritdoc} 84 | */ 85 | protected function initialize(InputInterface $input, OutputInterface $output) 86 | { 87 | $this->tmplDir = __DIR__ . '/../../../tmpl'; 88 | $this->questionHelper = $this->getHelper('question'); 89 | 90 | parent::initialize($input, $output); 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | protected function execute(InputInterface $input, OutputInterface $output) 97 | { 98 | $output->writeln('Install Console Jedi application'); 99 | 100 | $this->createEnvironmentsDir($input, $output); 101 | $this->createConfiguration($input, $output); 102 | 103 | $output->writeln('' . static::COMPLETED_LOGO . ''); 104 | } 105 | 106 | /** 107 | * Creates directory with environments settings. 108 | * 109 | * @param InputInterface $input 110 | * @param OutputInterface $output 111 | */ 112 | protected function createEnvironmentsDir(InputInterface $input, OutputInterface $output) 113 | { 114 | $targetDir = getcwd() . '/' . $this->envDir; 115 | $tmplDir = $this->tmplDir . '/environments'; 116 | 117 | $output->writeln(' - Environment settings'); 118 | 119 | if (file_exists($targetDir)) { 120 | $question = new ConfirmationQuestion( 121 | ' Directory ' . $targetDir . ' already exists' . PHP_EOL 122 | . ' Overwrite? [Y/n] ', 123 | true, 124 | '/^(y|j)/i' 125 | ); 126 | 127 | if (!$this->questionHelper->ask($input, $output, $question)) { 128 | return; 129 | } 130 | } 131 | 132 | $fs = new Filesystem(); 133 | $tmplIterator = new \RecursiveDirectoryIterator($tmplDir, \RecursiveDirectoryIterator::SKIP_DOTS); 134 | $iterator = new \RecursiveIteratorIterator($tmplIterator, \RecursiveIteratorIterator::SELF_FIRST); 135 | 136 | foreach ($iterator as $item) { 137 | $itemPath = $targetDir . '/' . $iterator->getSubPathName(); 138 | 139 | if ($item->isDir()) { 140 | $fs->mkdir($itemPath); 141 | } else { 142 | $fs->copy($item, $itemPath, true); 143 | } 144 | } 145 | 146 | $output->writeln(' Created directory settings of environments: ' . $targetDir . ''); 147 | } 148 | 149 | /** 150 | * Creates configuration file of application. 151 | * 152 | * @param InputInterface $input 153 | * @param OutputInterface $output 154 | */ 155 | protected function createConfiguration(InputInterface $input, OutputInterface $output) 156 | { 157 | $path = $this->getApplication()->getRoot() . '/.jedi.php'; 158 | 159 | $output->writeln(' - Configuration'); 160 | 161 | if (file_exists($path)) { 162 | $question = new ConfirmationQuestion( 163 | ' Configuration file ' . $path . ' already exists' . PHP_EOL 164 | . ' Overwrite? [Y/n] ', 165 | true, 166 | '/^(y|j)/i' 167 | ); 168 | 169 | if (!$this->questionHelper->ask($input, $output, $question)) { 170 | return; 171 | } 172 | } 173 | 174 | $fs = new Filesystem(); 175 | 176 | $question = new Question(' Enter path to web directory relative to ' 177 | . $this->getApplication()->getRoot() . ': ' . PHP_EOL 178 | . ' (or do not specify if you are already in the web directory)' . PHP_EOL); 179 | 180 | $question->setValidator(function ($answer) use ($fs) { 181 | $path = $answer; 182 | 183 | if ($answer === null) { 184 | $path = $this->getApplication()->getRoot(); 185 | } elseif (!$fs->isAbsolutePath($answer)) { 186 | $path = $this->getApplication()->getRoot() . '/' . $answer; 187 | } 188 | 189 | if (!is_dir($path)) { 190 | throw new \RuntimeException('Directory "' . $path . '" is missing'); 191 | } 192 | 193 | return $answer; 194 | }); 195 | 196 | $webDir = $this->questionHelper->ask($input, $output, $question); 197 | 198 | $content = file_get_contents($this->tmplDir . '/.jedi.php'); 199 | $content = str_replace( 200 | ['%web-dir%', '%env-dir%'], 201 | [addslashes($webDir), addslashes($this->envDir)], 202 | $content 203 | ); 204 | $fs->dumpFile($path, $content); 205 | 206 | $output->writeln(' Created configuration file of application ' . $path . ''); 207 | } 208 | } -------------------------------------------------------------------------------- /src/Application/Exception/BitrixException.php: -------------------------------------------------------------------------------- 1 | GetException() message. 13 | */ 14 | class BitrixException extends \RuntimeException 15 | { 16 | public static function hasException(\CMain $APPLICATION = null) 17 | { 18 | if (null === $APPLICATION) { 19 | $APPLICATION = $GLOBALS['APPLICATION']; 20 | } 21 | 22 | return is_object($APPLICATION->GetException()); 23 | } 24 | 25 | /** 26 | * Check for legacy bitrix exception, throws new self if any 27 | * 28 | * @param string $message [optional] Additional error message 29 | * @param \CMain $APPLICATION [optional] $APPLICATION instance 30 | * @throws static 31 | */ 32 | public static function generate($message = null, \CMain $APPLICATION = null) 33 | { 34 | if (null === $APPLICATION) { 35 | $APPLICATION = $GLOBALS['APPLICATION']; 36 | } 37 | 38 | if ($ex = $APPLICATION->GetException()) { 39 | throw new static($message ? $message . ': ' . $ex->GetString() : $ex->GetString()); 40 | } else { 41 | throw new static($message ? $message : 'Unknown exception'); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Application/Exception/ConfigurationException.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | class ConfigurationException extends \ErrorException 15 | { 16 | 17 | } -------------------------------------------------------------------------------- /src/Cache/Command/ClearCommand.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | class ClearCommand extends BitrixCommand 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function configure() 28 | { 29 | $this->setName('cache:clear') 30 | ->setDescription('Clear all cache') 31 | ->addOption('dir', 'd', InputOption::VALUE_REQUIRED, 'Clear cache only for directory (relative from /bitrix/cache/)') 32 | ->addOption('tag', 't', InputOption::VALUE_REQUIRED, 'Clear cache by tag'); 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | protected function execute(InputInterface $input, OutputInterface $output) 39 | { 40 | $dir = $input->getOption('dir'); 41 | $tag = $input->getOption('tag'); 42 | $cache = Cache::createInstance(); 43 | 44 | if (empty($dir) && empty($tag)) { 45 | Application::getInstance()->getManagedCache()->cleanAll(); 46 | $cache->cleanDir(); 47 | $cache->cleanDir(false, 'stack_cache'); 48 | StaticHtmlCache::getInstance()->deleteAll(); 49 | 50 | if (Cache::clearCache(true)) { 51 | $output->writeln('All Bitrix cache was deleted'); 52 | } else { 53 | $output->writeln('Error deleting Bitrix cache'); 54 | } 55 | } 56 | 57 | if ($dir) { 58 | $cache->cleanDir($dir); 59 | $output->writeln('Bitrix cache by "/' . BX_ROOT . '/cache/' . $dir . '" dir was deleted'); 60 | } 61 | 62 | if ($tag) { 63 | Application::getInstance()->getTaggedCache()->clearByTag($tag); 64 | $output->writeln('Bitrix cache by tag "' . $tag . '" was deleted'); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/Environment/Command/InitCommand.php: -------------------------------------------------------------------------------- 1 | 31 | */ 32 | class InitCommand extends Command 33 | { 34 | /** 35 | * @var string Path to the environment directory. 36 | */ 37 | protected $dir; 38 | /** 39 | * @var array Settings for current environment. The contents of the file `config.php`. 40 | */ 41 | protected $config = []; 42 | /** 43 | * @var array Methods which be running in the first place. 44 | */ 45 | protected $bootstrap = ['copyFiles', 'initializeBitrix']; 46 | /** 47 | * @var array Files that do not need to copy to the application when initializing the environment settings. 48 | */ 49 | protected $excludedFiles = ['config.php']; 50 | /** 51 | * @var string Type of the initialized environment. 52 | */ 53 | private $type; 54 | 55 | /** 56 | * {@inheritdoc} 57 | */ 58 | protected function configure() 59 | { 60 | parent::configure(); 61 | 62 | $this->setName('env:init') 63 | ->setDescription('Installation environment settings') 64 | ->setHelp('Run command and select environment from the list') 65 | ->addArgument('type', InputArgument::OPTIONAL, 'Type of the environments') 66 | ->addOption('memcache-cold-start', null, null, 'All memcache servers adds with status "ready"'); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | protected function initialize(InputInterface $input, OutputInterface $output) 73 | { 74 | parent::initialize($input, $output); 75 | 76 | if (!$this->getApplication()->getConfiguration()['env-dir']) { 77 | throw new \Exception('Config env-dir is missing'); 78 | } 79 | 80 | $dir = $this->getApplication()->getRoot() . '/' . $this->getApplication()->getConfiguration()['env-dir']; 81 | 82 | if (!is_dir($dir)) { 83 | throw new \Exception('Directory ' . $dir . ' is missing'); 84 | } 85 | 86 | $environments = include $dir . '/index.php'; 87 | 88 | if (!is_array($environments)) { 89 | throw new \Exception('File with description of environments is missing'); 90 | } elseif (count($environments) == 0) { 91 | throw new \Exception('Environments not found in description file'); 92 | } 93 | 94 | if ($input->getArgument('type')) { 95 | $type = $input->getArgument('type'); 96 | 97 | if (!isset($environments[$type])) { 98 | throw new \Exception('Invalid environment code!'); 99 | } 100 | } else { 101 | foreach ($environments as $type => $environment) { 102 | $choices[$type] = $environment['name']; 103 | } 104 | 105 | $questionHelper = $this->getHelper('question'); 106 | $question = new ChoiceQuestion('Which environment install?', $choices, false); 107 | $type = $questionHelper->ask($input, $output, $question); 108 | } 109 | 110 | if (!isset($environments[$type]['path'])) { 111 | throw new \Exception('Environment path not found!'); 112 | } 113 | 114 | $this->type = $type; 115 | $this->dir = $dir . '/' . $environments[$type]['path']; 116 | $this->config = include $this->dir . '/config.php'; 117 | } 118 | 119 | /** 120 | * {@inheritdoc} 121 | */ 122 | protected function execute(InputInterface $input, OutputInterface $output) 123 | { 124 | foreach ($this->bootstrap as $method) { 125 | $this->$method($input, $output); 126 | } 127 | 128 | foreach ($this->config as $config => $settings) { 129 | $method = 'configure' . ucfirst($config); 130 | 131 | if (!in_array($method, $this->bootstrap) && method_exists($this, $method)) { 132 | $output->writeln('Setup "' . $config . '"'); 133 | $this->$method($input, $output, $settings); 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * Copy files and directories from the environment directory to application. 140 | * 141 | * @param InputInterface $input 142 | * @param OutputInterface $output 143 | */ 144 | protected function copyFiles(InputInterface $input, OutputInterface $output) 145 | { 146 | $output->writeln('Copy files from the environment directory'); 147 | 148 | $fs = new Filesystem(); 149 | 150 | $directoryIterator = new \RecursiveDirectoryIterator($this->dir, \RecursiveDirectoryIterator::SKIP_DOTS); 151 | $iterator = new \RecursiveIteratorIterator($directoryIterator, \RecursiveIteratorIterator::SELF_FIRST); 152 | 153 | foreach ($iterator as $item) { 154 | if (in_array($iterator->getSubPathName(), $this->excludedFiles)) { 155 | continue; 156 | } 157 | 158 | $itemPath = $this->getApplication()->getRoot() . '/' . $iterator->getSubPathName(); 159 | 160 | if ($item->isDir()) { 161 | $fs->mkdir($itemPath); 162 | } else { 163 | $fs->copy($item, $itemPath, true); 164 | } 165 | 166 | $output->writeln(' ' . $itemPath); 167 | } 168 | } 169 | 170 | protected function initializeBitrix() 171 | { 172 | $this->getApplication()->initializeBitrix(); 173 | 174 | $connections = Configuration::getValue('connections'); 175 | 176 | foreach ($connections as $name => $parameters) { 177 | Application::getInstance()->getConnectionPool()->cloneConnection($name, $name, $parameters); 178 | } 179 | } 180 | 181 | /** 182 | * Sets license key Bitrix CMS. 183 | * 184 | * @param InputInterface $input 185 | * @param OutputInterface $output 186 | * @param string $licenseKey 187 | */ 188 | protected function configureLicenseKey(InputInterface $input, OutputInterface $output, $licenseKey) 189 | { 190 | if (!is_string($licenseKey)) { 191 | throw new \InvalidArgumentException('Config "licenseKey" must be string type.'); 192 | } 193 | 194 | $licenseFileContent = "<" . "? $" . "LICENSE_KEY = \"" . EscapePHPString($licenseKey) . "\"; ?" . ">"; 195 | File::putFileContents(Application::getDocumentRoot() . BX_ROOT . '/license_key.php', $licenseFileContent); 196 | } 197 | 198 | /** 199 | * Installation modules. 200 | * 201 | * @param InputInterface $input 202 | * @param OutputInterface $output 203 | * @param array $modules 204 | * @throws BitrixException 205 | * @throws \LogicException 206 | * @throws ModuleException 207 | */ 208 | protected function configureModules(InputInterface $input, OutputInterface $output, array $modules) 209 | { 210 | $app = $this->getApplication(); 211 | if ($app->getConfiguration()) { 212 | $app->addCommands(ModuleCommand::getCommands()); 213 | if ($app->getBitrixStatus() != \Notamedia\ConsoleJedi\Application\Application::BITRIX_STATUS_COMPLETE) { 214 | throw new BitrixException('Bitrix core is not available'); 215 | } 216 | } else { 217 | throw new BitrixException('No configuration loaded'); 218 | } 219 | 220 | if (!is_array($modules)) { 221 | throw new \LogicException('Incorrect modules configuration'); 222 | } 223 | 224 | if (!count($modules)) { 225 | return; 226 | } 227 | 228 | $bar = new ProgressBar($output, count($modules)); 229 | $bar->setRedrawFrequency(1); 230 | $bar->setFormat('verbose'); 231 | 232 | foreach ($modules as $moduleName) { 233 | $message = "\r" . ' module:load ' . $moduleName . ': '; 234 | 235 | try { 236 | if (isset($bar)) { 237 | $bar->setMessage($message); 238 | $bar->display(); 239 | } 240 | 241 | (new Module($moduleName))->load()->register(); 242 | 243 | $bar->clear(); 244 | $output->writeln($message . 'ok'); 245 | } catch (ModuleInstallException $e) { 246 | $bar->clear(); 247 | $output->writeln($e->getMessage(), OutputInterface::VERBOSITY_VERBOSE); 248 | $output->writeln($message . 'not registered (install it in admin panel)'); 249 | } catch (ModuleException $e) { 250 | $bar->clear(); 251 | $output->writeln($e->getMessage(), OutputInterface::VERBOSITY_VERBOSE); 252 | $output->writeln($message . 'FAILED'); 253 | } 254 | $bar->advance(); 255 | } 256 | $bar->finish(); 257 | $bar->clear(); 258 | $output->write("\r"); 259 | } 260 | 261 | /** 262 | * Sets configs to .settings.php. 263 | * 264 | * @param InputInterface $input 265 | * @param OutputInterface $output 266 | * @param array $settings 267 | */ 268 | protected function configureSettings(InputInterface $input, OutputInterface $output, array $settings) 269 | { 270 | $configuration = Configuration::getInstance(); 271 | 272 | foreach ($settings as $name => $value) { 273 | $configuration->setValue($name, $value); 274 | } 275 | } 276 | 277 | /** 278 | * Installation config to module "cluster". 279 | * 280 | * @param InputInterface $input 281 | * @param OutputInterface $output 282 | * @param array $cluster 283 | * 284 | * @throws \Bitrix\Main\LoaderException 285 | * @throws \Exception 286 | */ 287 | protected function configureCluster(InputInterface $input, OutputInterface $output, array $cluster) 288 | { 289 | global $APPLICATION; 290 | 291 | if (!Loader::includeModule('cluster')) { 292 | throw new \Exception('Failed to load module "cluster"'); 293 | } 294 | 295 | $memcache = new \CClusterMemcache; 296 | 297 | if (isset($cluster['memcache'])) { 298 | $output->writeln(' memcache'); 299 | 300 | if (!is_array($cluster['memcache'])) { 301 | throw new \Exception('Server info must be an array'); 302 | } 303 | 304 | $rsServers = $memcache->GetList(); 305 | 306 | while ($server = $rsServers->Fetch()) { 307 | $memcache->Delete($server['ID']); 308 | } 309 | 310 | foreach ($cluster['memcache'] as $index => $server) { 311 | $serverId = $memcache->Add($server); 312 | 313 | if ($serverId && !$input->getOption('memcache-cold-start')) { 314 | $memcache->Resume($serverId); 315 | } else { 316 | $exception = $APPLICATION->GetException(); 317 | $message = 'Invalid memcache config with index ' . $index; 318 | 319 | if ($exception->GetString()) { 320 | $message = str_replace('
', "\n", $exception->GetString()); 321 | } 322 | 323 | $output->writeln('' . $message . ''); 324 | } 325 | } 326 | } 327 | } 328 | 329 | /** 330 | * Installation of option modules. 331 | * 332 | * @param InputInterface $input 333 | * @param OutputInterface $output 334 | * @param array $options 335 | */ 336 | protected function configureOptions(InputInterface $input, OutputInterface $output, array $options) 337 | { 338 | if (empty($options)) { 339 | return; 340 | } 341 | 342 | foreach ($options as $module => $moduleOptions) { 343 | if (!is_array($moduleOptions) || empty($moduleOptions)) { 344 | continue; 345 | } 346 | 347 | foreach ($moduleOptions as $code => $value) { 348 | if (is_array($value)) { 349 | if (isset($value['value']) && isset($value['siteId'])) { 350 | Option::set($module, $code, $value['value'], $value['siteId']); 351 | } else { 352 | $output->writeln('Invalid option for module "' . $module . '" with code "' . $code . '"'); 353 | } 354 | } else { 355 | Option::set($module, $code, $value); 356 | 357 | $output->writeln(' option: "' . $code . '", module: "' . $module . '"'); 358 | } 359 | } 360 | } 361 | } 362 | 363 | /** 364 | * Gets type of initialized environment. 365 | * 366 | * @return string 367 | */ 368 | public function getType() 369 | { 370 | return $this->type; 371 | } 372 | } 373 | -------------------------------------------------------------------------------- /src/Iblock/Command/ExportCommand.php: -------------------------------------------------------------------------------- 1 | setName('iblock:export') 29 | ->setDescription('Export information block(s) to xml') 30 | ->addArgument( 31 | 'type', 32 | InputArgument::REQUIRED, 33 | 'Information block type' 34 | ) 35 | ->addArgument( 36 | 'code', 37 | InputArgument::REQUIRED, 38 | 'Information block code' 39 | ) 40 | ->addArgument( 41 | 'dir', 42 | InputArgument::OPTIONAL, 43 | 'Directory to export' 44 | ) 45 | ->addOption( 46 | 'sections', 47 | 's', 48 | InputOption::VALUE_OPTIONAL, 49 | 'Export sections [ "active", "all", "none" ]', 50 | 'none' 51 | ) 52 | ->addOption( 53 | 'elements', 54 | 'e', 55 | InputOption::VALUE_OPTIONAL, 56 | 'Export elements [ "active", "all", "none" ]', 57 | 'none' 58 | ); 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | protected function initialize(InputInterface $input, OutputInterface $output) 65 | { 66 | Loader::includeModule('iblock'); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | protected function interact(InputInterface $input, OutputInterface $output) 73 | { 74 | $this->setDir($input); 75 | $this->setIblocks($input); 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | protected function execute(InputInterface $input, OutputInterface $output) 82 | { 83 | $formatter = new FormatterHelper(); 84 | 85 | if (count($this->errors) > 0) { 86 | $output->writeln($formatter->formatBlock($this->errors, 'error')); 87 | return false; 88 | } 89 | 90 | $exporter = new Exporter(); 91 | $exporter 92 | ->setSections($input->getOption('sections')) 93 | ->setElements($input->getOption('elements')); 94 | 95 | foreach ($this->iblocks as $iblock) { 96 | 97 | try { 98 | $xml_id = \CIBlockCMLExport::GetIBlockXML_ID($iblock['ID']); 99 | $path = implode(DIRECTORY_SEPARATOR, [$this->dir, $xml_id]) . $this->extension; 100 | 101 | $exporter 102 | ->setPath($path) 103 | ->setId($iblock['ID']) 104 | ->execute(); 105 | 106 | $output->writeln(sprintf('%s iblock %s %s', 'success', $iblock['CODE'], $path)); 107 | 108 | } catch (IblockException $e) { 109 | $output->writeln(sprintf('%s iblock %s', 'fail', $iblock['CODE'])); 110 | if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { 111 | $output->writeln($e->getMessage()); 112 | } 113 | } 114 | } 115 | 116 | return true; 117 | } 118 | } -------------------------------------------------------------------------------- /src/Iblock/Command/ImportCommand.php: -------------------------------------------------------------------------------- 1 | setName('iblock:import') 29 | ->setDescription('Import information block(s) from xml') 30 | ->addArgument( 31 | 'type', 32 | InputArgument::REQUIRED, 33 | 'Information block type' 34 | ) 35 | ->addArgument( 36 | 'sites', 37 | InputArgument::REQUIRED, 38 | 'Sites to which the information block will be bound (if it is to be created)' 39 | ) 40 | ->addArgument( 41 | 'dir', 42 | InputArgument::OPTIONAL, 43 | 'Directory to import' 44 | ) 45 | ->addOption( 46 | 'sections', 47 | 's', 48 | InputOption::VALUE_OPTIONAL, 49 | 'If an existing section is no longer in the source file [ leave: "N", deactivate: "A", delete: "D" ]', 50 | 'A' 51 | ) 52 | ->addOption( 53 | 'elements', 54 | 'e', 55 | InputOption::VALUE_OPTIONAL, 56 | 'If an existing element is no longer in the source file [ leave: "N", deactivate: "A", delete: "D" ]', 57 | 'A' 58 | ); 59 | 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | protected function initialize(InputInterface $input, OutputInterface $output) 66 | { 67 | Loader::includeModule('iblock'); 68 | } 69 | 70 | 71 | /** 72 | * {@inheritdoc} 73 | */ 74 | protected function interact(InputInterface $input, OutputInterface $output) 75 | { 76 | $this->setDir($input); 77 | $this->setType($input); 78 | $this->setSites($input); 79 | } 80 | 81 | /** 82 | * {@inheritdoc} 83 | */ 84 | protected function execute(InputInterface $input, OutputInterface $output) 85 | { 86 | $formatter = new FormatterHelper(); 87 | 88 | if (count($this->errors) > 0) { 89 | $output->writeln($formatter->formatBlock($this->errors, 'error')); 90 | return false; 91 | } 92 | 93 | $importer = new Importer(); 94 | $importer 95 | ->setType($this->type) 96 | ->setSites($this->sites) 97 | ->setActionSection($input->getOption('sections')) 98 | ->setActionElement($input->getOption('elements'));; 99 | 100 | foreach (glob(implode(DIRECTORY_SEPARATOR . '*', [$this->dir, $this->extension])) as $file) { 101 | 102 | try { 103 | $importer 104 | ->setPath($file) 105 | ->execute(); 106 | 107 | $output->writeln(sprintf('%s file %s', 'success', $file)); 108 | 109 | } catch (IblockException $e) { 110 | $output->writeln(sprintf('%s file %s', 'fail', $file)); 111 | if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { 112 | $output->writeln($e->getMessage()); 113 | } 114 | } 115 | } 116 | } 117 | } -------------------------------------------------------------------------------- /src/Iblock/Command/MigrationCommandTrait.php: -------------------------------------------------------------------------------- 1 | dir 61 | * 62 | * @param InputInterface $input 63 | */ 64 | protected function setDir(InputInterface $input) 65 | { 66 | $app = new Application(); 67 | $filesystem = new Filesystem(); 68 | $dir = $input->getArgument('dir'); 69 | 70 | if (!$dir) { 71 | $dir = $app->getRoot(); 72 | } elseif (!$filesystem->isAbsolutePath($dir)) { 73 | $dir = $app->getRoot() . DIRECTORY_SEPARATOR . $dir; 74 | } 75 | $dir = rtrim($dir, DIRECTORY_SEPARATOR); 76 | 77 | if (!$filesystem->exists($dir)) { 78 | $this->errors[] = "Directory $dir not found"; 79 | } 80 | 81 | $this->dir = $dir; 82 | } 83 | 84 | /** 85 | * Check arguments type and code, set $this->iblocks 86 | * 87 | * @param InputInterface $input 88 | */ 89 | protected function setIblocks(InputInterface $input) 90 | { 91 | $iblocks = IblockTable::query() 92 | ->setFilter([ 93 | 'IBLOCK_TYPE_ID' => $input->getArgument('type'), 94 | 'CODE' => $input->getArgument('code') 95 | ]) 96 | ->setSelect(['ID', 'CODE']) 97 | ->exec(); 98 | 99 | if ($iblocks->getSelectedRowsCount() <= 0) { 100 | $this->errors[] = 'Iblock(s) not found'; 101 | } 102 | 103 | $this->iblocks = $iblocks->fetchAll(); 104 | } 105 | 106 | /** 107 | * Check argument type and set $this->type 108 | * 109 | * @param InputInterface $input 110 | */ 111 | protected function setType(InputInterface $input) 112 | { 113 | $type = TypeTable::query() 114 | ->setFilter([ 115 | 'ID' => $input->getArgument('type'), 116 | ]) 117 | ->setSelect(['ID']) 118 | ->exec(); 119 | 120 | if ($type->getSelectedRowsCount() <= 0) { 121 | $this->errors[] = 'Type not found'; 122 | } 123 | 124 | $this->type = $type->fetch()['ID']; 125 | } 126 | 127 | /** 128 | * Check argument sites and set $this->sites 129 | * 130 | * @param InputInterface $input 131 | */ 132 | protected function setSites(InputInterface $input) 133 | { 134 | $sites = SiteTable::query() 135 | ->setFilter([ 136 | 'LID' => $input->getArgument('sites'), 137 | ]) 138 | ->setSelect(['LID']) 139 | ->exec(); 140 | 141 | if ($sites->getSelectedRowsCount() <= 0) { 142 | $this->errors[] = 'Sites not found'; 143 | } 144 | 145 | $this->sites = $sites->fetchAll(); 146 | } 147 | } -------------------------------------------------------------------------------- /src/Iblock/Exception/ExportException.php: -------------------------------------------------------------------------------- 1 | config = [ 39 | 'id' => '', 40 | 'path' => '', 41 | 'sections' => 'none', 42 | 'elements' => 'none', 43 | 'interval' => 0 44 | ]; 45 | 46 | Loader::includeModule('iblock'); 47 | $this->export = new \CIBlockCMLExport(); 48 | } 49 | 50 | /** 51 | * Set id information block 52 | * 53 | * @param int $id 54 | * @return $this 55 | */ 56 | public function setId($id) 57 | { 58 | $this->config['id'] = intval($id); 59 | return $this; 60 | } 61 | 62 | /** 63 | * Set file path to export 64 | * 65 | * @param string $path 66 | * @return $this 67 | */ 68 | public function setPath($path) 69 | { 70 | $this->config['path'] = $path; 71 | return $this; 72 | } 73 | 74 | /** 75 | * Set settings export sections 76 | * 77 | * @param string $sections 78 | * @return $this 79 | */ 80 | public function setSections($sections) 81 | { 82 | $this->config['sections'] = $sections; 83 | return $this; 84 | } 85 | 86 | /** 87 | * Set settings export elements 88 | * 89 | * @param string $elements 90 | * @return $this 91 | */ 92 | public function setElements($elements) 93 | { 94 | $this->config['elements'] = $elements; 95 | return $this; 96 | } 97 | 98 | /** 99 | * @inheritdoc 100 | */ 101 | public function execute() 102 | { 103 | $pathinfo = pathinfo($this->config['path']); 104 | $this->session = [ 105 | "property_map" => false, 106 | "section_map" => false, 107 | "work_dir" => $pathinfo['dirname'] . DIRECTORY_SEPARATOR, 108 | "file_dir" => $pathinfo['filename'] . "_files" . DIRECTORY_SEPARATOR, 109 | ]; 110 | 111 | $this->export(); 112 | } 113 | 114 | /** 115 | * Direct export 116 | * 117 | * @return $this 118 | * @throws ExportException 119 | */ 120 | protected function export() 121 | { 122 | $filesystem = new Filesystem(); 123 | $handle = fopen($this->config['path'] . $this->prefix, "w"); 124 | 125 | $checkPermissions = true; 126 | if (PHP_SAPI == 'cli') { 127 | $checkPermissions = false; 128 | } 129 | 130 | if (!$this->export->Init( 131 | $handle, 132 | $this->config["id"], 133 | false, 134 | true, 135 | $this->session["work_dir"], 136 | $this->session["file_dir"], 137 | $checkPermissions 138 | ) 139 | ) { 140 | throw new ExportException('Failed to initialize export'); 141 | } 142 | 143 | $this->export->DoNotDownloadCloudFiles(); 144 | $this->export->StartExport(); 145 | 146 | $this->export->StartExportMetadata(); 147 | $this->export->ExportProperties($this->session["property_map"]); 148 | $this->export->ExportSections( 149 | $this->session["section_map"], 150 | time(), 151 | $this->config['interval'], 152 | $this->config["sections"], 153 | $this->session["property_map"] 154 | ); 155 | $this->export->EndExportMetadata(); 156 | 157 | $this->export->StartExportCatalog(); 158 | $this->export->ExportElements( 159 | $this->session["property_map"], 160 | $this->session["section_map"], 161 | time(), 162 | $this->config['interval'], 163 | 0, 164 | $this->config["elements"] 165 | ); 166 | $this->export->EndExportCatalog(); 167 | 168 | $this->export->ExportProductSets(); 169 | $this->export->EndExport(); 170 | 171 | fclose($handle); 172 | $filesystem->remove($this->config['path']); 173 | $filesystem->rename($this->config['path'] . $this->prefix, $this->config['path'], true); 174 | } 175 | } -------------------------------------------------------------------------------- /src/Iblock/Importer.php: -------------------------------------------------------------------------------- 1 | config = [ 37 | 'type' => '', 38 | 'lids' => [], 39 | 'path' => '', 40 | 'action_section' => 'A', 41 | 'action_element' => 'A', 42 | 'preview' => 'Y', 43 | 'detail' => 'Y', 44 | 'interval' => 0 45 | ]; 46 | 47 | Loader::includeModule('iblock'); 48 | $this->xml = new \CIBlockXMLFile(); 49 | $this->import = new \CIBlockCMLImport(); 50 | 51 | } 52 | 53 | /** 54 | * Set type information block 55 | * 56 | * @param string $type 57 | * @return $this 58 | */ 59 | public function setType($type) 60 | { 61 | $this->config['type'] = $type; 62 | return $this; 63 | } 64 | 65 | /** 66 | * Set file path to import 67 | * 68 | * @param string $path 69 | * @return $this 70 | */ 71 | public function setPath($path) 72 | { 73 | $this->config['path'] = $path; 74 | return $this; 75 | } 76 | 77 | /** 78 | * Set binding sites for importing information block 79 | * @param array $lids 80 | * @return $this 81 | */ 82 | public function setSites($lids) 83 | { 84 | $this->config['lids'] = $lids; 85 | return $this; 86 | } 87 | 88 | /** 89 | * What doing with existing section 90 | * 91 | * @param string $action 92 | * @return $this 93 | */ 94 | public function setActionSection($action) 95 | { 96 | $this->config['action_section'] = $action; 97 | return $this; 98 | } 99 | 100 | /** 101 | * What doing with existing element 102 | * 103 | * @param string $action 104 | * @return $this 105 | */ 106 | public function setActionElement($action) 107 | { 108 | $this->config['action_element'] = $action; 109 | return $this; 110 | } 111 | 112 | /** 113 | * @inheritdoc 114 | */ 115 | public function execute() 116 | { 117 | $this->session = [ 118 | "section_map" => false, 119 | "prices_map" => false, 120 | "work_dir" => pathinfo($this->config['path'], PATHINFO_DIRNAME) . DIRECTORY_SEPARATOR 121 | ]; 122 | 123 | $this->read(); 124 | $this->import(); 125 | } 126 | 127 | /** 128 | * Read file 129 | * 130 | * @throws ImportException 131 | */ 132 | protected function read() 133 | { 134 | $handle = fopen($this->config['path'], "r"); 135 | 136 | if (!$handle) 137 | throw new ImportException('Unable to open file, or file not exist'); 138 | 139 | if (!$this->import->CheckIfFileIsCML($this->config['path'])) { 140 | throw new ImportException('File is not valid'); 141 | } 142 | 143 | $this->xml->DropTemporaryTables(); 144 | $this->xml->CreateTemporaryTables(); 145 | $this->xml->ReadXMLToDatabase($handle, $this->session, $this->config['interval']); 146 | $this->xml->IndexTemporaryTables(); 147 | } 148 | 149 | /** 150 | * Direct import 151 | */ 152 | protected function import() 153 | { 154 | $this->import->Init( 155 | $this->config, 156 | $this->session['work_dir'], 157 | true, 158 | $this->config["preview"], 159 | $this->config["detail"], 160 | true 161 | ); 162 | $this->import->ImportMetaData([1, 2], $this->config["type"], $this->config["lids"]); 163 | 164 | $this->import->ImportSections(); 165 | $this->import->DeactivateSections($this->config["action_section"]); 166 | 167 | $this->import->ReadCatalogData($this->session["section_map"], $this->session["prices_map"]); 168 | $this->import->ImportElements(time(), $this->config["interval"]); 169 | $this->import->DeactivateElement($this->config["action_element"], time(), $this->config["interval"]); 170 | 171 | $this->import->ImportProductSets(); 172 | } 173 | } -------------------------------------------------------------------------------- /src/Iblock/MigrationInterface.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class LoadCommand extends ModuleCommand 22 | { 23 | use CanRestartTrait; 24 | 25 | /** 26 | * {@inheritdoc} 27 | */ 28 | protected function configure() 29 | { 30 | parent::configure(); 31 | 32 | $this->setName('module:load') 33 | ->setDescription('Load and install module from Marketplace') 34 | ->addOption('no-update', 'nu', InputOption::VALUE_NONE, 'Don\' update module') 35 | ->addOption('no-register', 'ni', InputOption::VALUE_NONE, 'Load only, don\' register module') 36 | ->addOption('beta', 'b', InputOption::VALUE_NONE, 'Allow the installation of beta releases'); 37 | } 38 | 39 | /** 40 | * {@inheritdoc} 41 | */ 42 | protected function execute(InputInterface $input, OutputInterface $output) 43 | { 44 | $module = new Module($input->getArgument('module')); 45 | 46 | if (!$module->isThirdParty()) { 47 | $output->writeln('Loading kernel modules is unsupported'); 48 | } 49 | 50 | if ($input->getOption('beta')) { 51 | $module->setBeta(); 52 | } 53 | 54 | $module->load(); 55 | 56 | if (!$input->getOption('no-update')) { 57 | $modulesUpdated = null; 58 | while ($module->update($modulesUpdated)) { 59 | if (is_array($modulesUpdated)) { 60 | foreach ($modulesUpdated as $moduleName => $moduleVersion) { 61 | $output->writeln(sprintf('updated %s to %s', $moduleName, $moduleVersion)); 62 | } 63 | } 64 | return $this->restartScript($input, $output); 65 | } 66 | } 67 | 68 | if (!$input->getOption('no-register')) { 69 | try { 70 | $module->register(); 71 | } catch (ModuleInstallException $e) { 72 | $output->writeln(sprintf('%s', $e->getMessage()), 73 | OutputInterface::VERBOSITY_VERBOSE); 74 | $output->writeln(sprintf('Module loaded, but not registered. You need to do it yourself in admin panel.', 75 | $module->getName())); 76 | } 77 | } 78 | 79 | $output->writeln(sprintf('installed %s', $module->getName())); 80 | 81 | return 0; 82 | } 83 | } -------------------------------------------------------------------------------- /src/Module/Command/ModuleCommand.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | abstract class ModuleCommand extends BitrixCommand 23 | { 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function configure() 28 | { 29 | parent::configure(); 30 | 31 | $this->addArgument('module', InputArgument::REQUIRED, 'Module name (e.g. `vendor.module`)') 32 | ->addOption('confirm-thirdparty', 'ct', InputOption::VALUE_NONE, 'Suppress third-party modules warning'); 33 | } 34 | 35 | /** 36 | * @inheritdoc 37 | */ 38 | protected function interact(InputInterface $input, OutputInterface $output) 39 | { 40 | parent::interact($input, $output); 41 | 42 | $module = new Module($input->getArgument('module')); 43 | 44 | if (in_array($this->getName(), ['module:register', 'module:unregister']) 45 | && $module->isThirdParty() && !$input->getOption('confirm-thirdparty') 46 | ) { 47 | $output->writeln($module->isThirdParty() . ' is not a kernel module. Correct operation cannot be guaranteed for third-party modules!'); 48 | } 49 | } 50 | 51 | /** 52 | * Gets console commands from this package. 53 | * 54 | * @return Command[] 55 | */ 56 | public static function getCommands() 57 | { 58 | return [ 59 | new LoadCommand(), 60 | new RegisterCommand(), 61 | new RemoveCommand(), 62 | new UnregisterCommand(), 63 | new UpdateCommand(), 64 | ]; 65 | } 66 | } -------------------------------------------------------------------------------- /src/Module/Command/RegisterCommand.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class RegisterCommand extends ModuleCommand 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function configure() 24 | { 25 | parent::configure(); 26 | 27 | $this->setName('module:register') 28 | ->setDescription('Install module'); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function execute(InputInterface $input, OutputInterface $output) 35 | { 36 | $module = new Module($input->getArgument('module')); 37 | $module->register(); 38 | $output->writeln(sprintf('registered %s', $module->getName())); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Module/Command/RemoveCommand.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class RemoveCommand extends ModuleCommand 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function configure() 24 | { 25 | parent::configure(); 26 | 27 | $this->setName('module:remove') 28 | ->setDescription('Uninstall and remove module folder from system'); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function execute(InputInterface $input, OutputInterface $output) 35 | { 36 | $module = new Module($input->getArgument('module')); 37 | $module->remove(); 38 | $output->writeln(sprintf('removed %s', $module->getName())); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Module/Command/UnregisterCommand.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class UnregisterCommand extends ModuleCommand 19 | { 20 | /** 21 | * {@inheritdoc} 22 | */ 23 | protected function configure() 24 | { 25 | parent::configure(); 26 | 27 | $this->setName('module:unregister') 28 | ->setDescription('Uninstall module'); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | protected function execute(InputInterface $input, OutputInterface $output) 35 | { 36 | $module = new Module($input->getArgument('module')); 37 | $module->unRegister(); 38 | $output->writeln(sprintf('unregistered %s', $module->getName())); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Module/Command/UpdateCommand.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | class UpdateCommand extends ModuleCommand 21 | { 22 | use CanRestartTrait; 23 | 24 | /** 25 | * {@inheritdoc} 26 | */ 27 | protected function configure() 28 | { 29 | parent::configure(); 30 | 31 | $this->setName('module:update') 32 | ->setDescription('Load module updates from Marketplace') 33 | ->addOption('beta', 'b', InputOption::VALUE_NONE, 'Allow the installation of beta releases'); 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | protected function execute(InputInterface $input, OutputInterface $output) 40 | { 41 | $module = new Module($input->getArgument('module')); 42 | $modulesUpdated = null; 43 | while ($module->update($modulesUpdated)) 44 | { 45 | if (is_array($modulesUpdated)) 46 | { 47 | foreach ($modulesUpdated as $moduleName => $moduleVersion) 48 | { 49 | $output->writeln(sprintf('updated %s to %s', $moduleName, $moduleVersion)); 50 | } 51 | } 52 | return $this->restartScript($input, $output); 53 | } 54 | return 0; 55 | } 56 | } -------------------------------------------------------------------------------- /src/Module/Exception/ModuleException.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | class Module 20 | { 21 | /** 22 | * @var string 23 | */ 24 | private $name; 25 | /** 26 | * @var \CModule 27 | */ 28 | private $object; 29 | /** 30 | * @var bool 31 | */ 32 | private $beta = false; 33 | 34 | /** 35 | * @param string $moduleName 36 | */ 37 | public function __construct($moduleName) 38 | { 39 | $this->name = $this->normalizeName($moduleName); 40 | } 41 | 42 | /** 43 | * @param string $moduleName 44 | * @return string 45 | */ 46 | protected function normalizeName($moduleName) 47 | { 48 | return preg_replace("/[^a-zA-Z0-9_.]+/i", "", trim($moduleName)); 49 | } 50 | 51 | /** 52 | * @return \CModule 53 | */ 54 | protected function &getObject() 55 | { 56 | if (!isset($this->object)) { 57 | $this->object = \CModule::CreateModuleObject($this->name); 58 | } 59 | 60 | if (!is_object($this->object) || !($this->object instanceof \CModule)) { 61 | unset($this->object); 62 | throw new Exception\ModuleNotFoundException('Module not found or incorrect', $this->name); 63 | } 64 | 65 | return $this->object; 66 | } 67 | 68 | /** 69 | * Checks for module and module object existence. 70 | * 71 | * @return bool 72 | */ 73 | public function exist() 74 | { 75 | try { 76 | $this->getObject(); 77 | } catch (Exception\ModuleNotFoundException $e) { 78 | return false; 79 | } 80 | 81 | return true; 82 | } 83 | 84 | /** 85 | * Check if module exists and installed 86 | * 87 | * @return bool 88 | */ 89 | public function isRegistered() 90 | { 91 | return ModuleManager::isModuleInstalled($this->name) && $this->exist(); 92 | } 93 | 94 | /** 95 | * @return bool true for marketplace modules, false for kernel modules 96 | */ 97 | public function isThirdParty() 98 | { 99 | return strpos($this->name, '.') !== false; 100 | } 101 | 102 | /** 103 | * Install module. 104 | * 105 | * @throws Exception\ModuleException 106 | * @throws BitrixException 107 | */ 108 | public function register() 109 | { 110 | if (!$this->isRegistered()) { 111 | $moduleObject =& $this->getObject(); 112 | 113 | /** 114 | * It's important to check if module class defines InstallDB method (it must register module) 115 | * Thus absent InstallDB indicates that the module does not support automatic installation 116 | */ 117 | if ((new \ReflectionClass($moduleObject))->getMethod('InstallDB')->class !== get_class($moduleObject)) { 118 | throw new Exception\ModuleInstallException( 119 | 'Missing InstallDB method. This module does not support automatic installation', 120 | $this->name 121 | ); 122 | } 123 | 124 | if (!$moduleObject->InstallDB() && BitrixException::hasException()) { 125 | throw new Exception\ModuleInstallException( 126 | get_class($moduleObject) . '::InstallDB() returned false', 127 | $this->name 128 | ); 129 | } 130 | 131 | $moduleObject->InstallEvents(); 132 | 133 | /** @noinspection PhpVoidFunctionResultUsedInspection */ 134 | if (!$moduleObject->InstallFiles() && BitrixException::hasException()) { 135 | throw new Exception\ModuleInstallException( 136 | get_class($moduleObject) . '::InstallFiles() returned false', 137 | $this->name 138 | ); 139 | } 140 | 141 | if (!$this->isRegistered()) { 142 | throw new Exception\ModuleInstallException( 143 | 'Module was not registered. Probably it does not support automatic installation.', 144 | $this->name 145 | ); 146 | } 147 | } 148 | 149 | return $this; 150 | } 151 | 152 | /** 153 | * Download module from Marketplace. 154 | * 155 | * @return $this 156 | */ 157 | public function load() 158 | { 159 | if (!$this->isRegistered()) { 160 | if (!$this->exist()) { 161 | require_once($_SERVER["DOCUMENT_ROOT"] . '/bitrix/modules/main/classes/general/update_client_partner.php'); 162 | 163 | if (!\CUpdateClientPartner::LoadModuleNoDemand( 164 | $this->getName(), 165 | $strError, 166 | $this->isBeta() ? 'N' : 'Y', 167 | LANGUAGE_ID) 168 | ) { 169 | throw new Exception\ModuleLoadException($strError, $this->getName()); 170 | } 171 | } 172 | } 173 | 174 | return $this; 175 | } 176 | 177 | /** 178 | * Uninstall module. 179 | * 180 | * @throws Exception\ModuleException 181 | * @throws BitrixException 182 | */ 183 | public function unRegister() 184 | { 185 | $moduleObject = $this->getObject(); 186 | 187 | if ($this->isRegistered()) { 188 | /** 189 | * It's important to check if module class defines UnInstallDB method (it should unregister module) 190 | * Thus absent UnInstallDB indicates that the module does not support automatic uninstallation 191 | */ 192 | if ((new \ReflectionClass($moduleObject))->getMethod('UnInstallDB')->class !== get_class($moduleObject)) { 193 | throw new Exception\ModuleUninstallException( 194 | 'Missing UnInstallDB method. This module does not support automatic uninstallation', 195 | $this->name 196 | ); 197 | } 198 | 199 | /** @noinspection PhpVoidFunctionResultUsedInspection */ 200 | if (!$moduleObject->UnInstallFiles() && BitrixException::hasException()) { 201 | throw new Exception\ModuleUninstallException( 202 | get_class($moduleObject) . '::UnInstallFiles() returned false', 203 | $this->name 204 | ); 205 | } 206 | 207 | $moduleObject->UnInstallEvents(); 208 | 209 | /** @noinspection PhpVoidFunctionResultUsedInspection */ 210 | if (!$moduleObject->UnInstallDB() && BitrixException::hasException()) { 211 | throw new Exception\ModuleUninstallException( 212 | get_class($moduleObject) . '::UnInstallFiles() returned false', 213 | $this->name 214 | ); 215 | } 216 | 217 | if ($this->isRegistered()) { 218 | throw new Exception\ModuleUninstallException('Module was not unregistered', $this->name); 219 | } 220 | } 221 | 222 | return $this; 223 | } 224 | 225 | /** 226 | * Uninstall and remove module directory. 227 | */ 228 | public function remove() 229 | { 230 | if ($this->isRegistered()) { 231 | $this->unRegister(); 232 | } 233 | 234 | $path = getLocalPath('modules/' . $this->getName()); 235 | 236 | if ($path) { 237 | (new Filesystem())->remove($_SERVER['DOCUMENT_ROOT'] . $path); 238 | } 239 | 240 | unset($this->object); 241 | 242 | return $this; 243 | } 244 | 245 | /** 246 | * Update module. 247 | * 248 | * It must be called repeatedly until the method returns false. 249 | * After each call php must be restarted (new process created) to update module class and function definitions. 250 | * 251 | * @param array $modulesUpdated [optional] 252 | * @return bool 253 | */ 254 | public function update(&$modulesUpdated = null) 255 | { 256 | require_once($_SERVER["DOCUMENT_ROOT"] . '/bitrix/modules/main/classes/general/update_client_partner.php'); 257 | 258 | if (!$this->isThirdParty()) { 259 | throw new Exception\ModuleUpdateException('Kernel module updates are currently not supported.', 260 | $this->getName()); 261 | } 262 | 263 | // ensures module existence 264 | $this->getObject(); 265 | 266 | $errorMessage = $updateDescription = null; 267 | $loadResult = \CUpdateClientPartner::LoadModulesUpdates( 268 | $errorMessage, 269 | $updateDescription, 270 | LANGUAGE_ID, 271 | $this->isBeta() ? 'N' : 'Y', 272 | [$this->getName()], 273 | true 274 | ); 275 | switch ($loadResult) { 276 | // archive loaded 277 | case "S": 278 | return $this->update($modulesUpdated); 279 | 280 | // error 281 | case "E": 282 | throw new Exception\ModuleUpdateException($errorMessage, $this->getName()); 283 | 284 | // finished installing updates 285 | case "F": 286 | return false; 287 | 288 | // need to process loaded update 289 | case 'U': 290 | break; 291 | } 292 | 293 | /** @var string Temp directory with update files */ 294 | $updateDir = null; 295 | 296 | if (!\CUpdateClientPartner::UnGzipArchive($updateDir, $errorMessage, true)) { 297 | throw new Exception\ModuleUpdateException('[CL02] UnGzipArchive failed. ' . $errorMessage, 298 | $this->getName()); 299 | } 300 | 301 | $this->validateUpdate($updateDir); 302 | 303 | if (isset($updateDescription["DATA"]["#"]["NOUPDATES"])) { 304 | \CUpdateClientPartner::ClearUpdateFolder($_SERVER["DOCUMENT_ROOT"] . "/bitrix/updates/" . $updateDir); 305 | return false; 306 | } 307 | 308 | $modulesUpdated = $updateDescr = []; 309 | if (isset($updateDescription["DATA"]["#"]["ITEM"])) { 310 | foreach ($updateDescription["DATA"]["#"]["ITEM"] as $moduleInfo) { 311 | $modulesUpdated[$moduleInfo["@"]["NAME"]] = $moduleInfo["@"]["VALUE"]; 312 | $updateDescr[$moduleInfo["@"]["NAME"]] = $moduleInfo["@"]["DESCR"]; 313 | } 314 | } 315 | 316 | if (\CUpdateClientPartner::UpdateStepModules($updateDir, $errorMessage)) { 317 | foreach ($modulesUpdated as $key => $value) { 318 | if (Option::set('main', 'event_log_marketplace', "Y") === "Y") { 319 | \CEventLog::Log("INFO", "MP_MODULE_DOWNLOADED", "main", $key, $value); 320 | } 321 | } 322 | } else { 323 | throw new Exception\ModuleUpdateException('[CL04] UpdateStepModules failed. ' . $errorMessage, 324 | $this->getName()); 325 | } 326 | 327 | return true; 328 | } 329 | 330 | /** 331 | * Check update files. 332 | * 333 | * @param string $updateDir 334 | */ 335 | protected function validateUpdate($updateDir) 336 | { 337 | $errorMessage = null; 338 | if (!\CUpdateClientPartner::CheckUpdatability($updateDir, $errorMessage)) { 339 | throw new Exception\ModuleUpdateException('[CL03] CheckUpdatability failed. ' . $errorMessage, 340 | $this->getName()); 341 | } 342 | 343 | if (isset($updateDescription["DATA"]["#"]["ERROR"])) { 344 | $errorMessage = ""; 345 | foreach ($updateDescription["DATA"]["#"]["ERROR"] as $errorDescription) { 346 | $errorMessage .= "[" . $errorDescription["@"]["TYPE"] . "] " . $errorDescription["#"]; 347 | } 348 | throw new Exception\ModuleUpdateException($errorMessage, $this->getName()); 349 | } 350 | } 351 | 352 | /** 353 | * Returns module name. 354 | * 355 | * @return string 356 | */ 357 | public function getName() 358 | { 359 | return $this->name; 360 | } 361 | 362 | /** 363 | * Beta releases allowed? 364 | * 365 | * @return boolean 366 | */ 367 | public function isBeta() 368 | { 369 | return $this->beta; 370 | } 371 | 372 | /** 373 | * Set beta releases installation. 374 | * 375 | * @param boolean $beta 376 | */ 377 | public function setBeta($beta = true) 378 | { 379 | $this->beta = $beta; 380 | } 381 | 382 | public function getVersion() 383 | { 384 | return $this->getObject()->MODULE_VERSION; 385 | } 386 | } -------------------------------------------------------------------------------- /src/Search/Command/ReIndexCommand.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | class ReIndexCommand extends BitrixCommand 24 | { 25 | const UPDATE_TIME = 5; 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | protected function configure() 31 | { 32 | parent::configure(); 33 | 34 | $this->setName('search:reindex') 35 | ->setDescription('Rebuild search index') 36 | ->addOption('full', 'f', InputOption::VALUE_NONE, 37 | 'Clears existing index (otherwise only changed entries would be indexed)'); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function execute(InputInterface $input, OutputInterface $output) 44 | { 45 | if (!Loader::includeModule('search')) { 46 | throw new BitrixException('Search module is not installed'); 47 | } 48 | 49 | $searchResult = array(); 50 | 51 | $bar = new ProgressBar($output, 0); 52 | do { 53 | $bar->display(); 54 | 55 | $searchResult = \CSearch::ReIndexAll($input->getOption('full'), static::UPDATE_TIME, $searchResult); 56 | 57 | $bar->advance(); 58 | $bar->clear(); 59 | 60 | if (is_array($searchResult) && $searchResult['MODULE'] == 'main') { 61 | list(, $path) = explode("|", $searchResult["ID"], 2); 62 | $output->writeln("\r " . $path, OutputInterface::VERBOSITY_VERBOSE); 63 | } 64 | } while (is_array($searchResult)); 65 | 66 | $bar->finish(); 67 | $bar->clear(); 68 | $output->write("\r"); 69 | 70 | if (ModuleManager::isModuleInstalled('socialnetwork')) { 71 | $output->writeln('The Social Network module needs to be reindexed using the Social Network component in the public section of site.'); 72 | } 73 | 74 | $output->writeln(sprintf('Reindexed %d element%s.', $searchResult, $searchResult > 1 ? 's' : '')); 75 | 76 | return 0; 77 | } 78 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | autoloadTests(); -------------------------------------------------------------------------------- /tmpl/.jedi.php: -------------------------------------------------------------------------------- 1 | '%web-dir%', 16 | 'env-dir' => '%env-dir%', 17 | 'useModules' => true, 18 | 'commands' => [ 19 | ] 20 | ]; -------------------------------------------------------------------------------- /tmpl/environments/dev/config.php: -------------------------------------------------------------------------------- 1 | 'NFR-123-456', 9 | * 10 | * // Modules to be installed. 11 | * // Warning: install the modules using DB migration. Install the modules 12 | * // using the settings of the environment, only for dev environment. 13 | * 'modules' => [ 14 | * 'vendor.debug' 15 | * ], 16 | * 17 | * // Options for modules 18 | * 'options' => [ 19 | * 'vendor.module' => [ 20 | * 'OPTION_CODE' => 'value', 21 | * 'OPTION_CODE' => ['value' => 'test', 'siteId' => 's1'] 22 | * ], 23 | * ], 24 | * 25 | * // Settings for module "cluster" 26 | * 'cluster' => [ 27 | * 'memcache' => [ 28 | * [ 29 | * 'GROUP_ID' => 1, 30 | * 'HOST' => 'host', 31 | * 'PORT' => 'port', 32 | * 'WEIGHT' => 'weight', 33 | * 'STATUS' => 'status', 34 | * ], 35 | * [ 36 | * 'GROUP_ID' => 1, 37 | * 'HOST' => 'host', 38 | * 'PORT' => 'port', 39 | * 'WEIGHT' => 'weight', 40 | * 'STATUS' => 'status', 41 | * ] 42 | * ] 43 | * ], 44 | * 45 | * // Values for file .settings.php 46 | * 'settings' => [ 47 | * 'connections' => [ 48 | * 'default' => [ 49 | * 'host' => 'host', 50 | * 'database' => 'db', 51 | * 'login' => 'login', 52 | * 'password' => 'pass', 53 | * 'className' => '\\Bitrix\\Main\\DB\\MysqlConnection', 54 | * 'options' => 2, 55 | * ], 56 | * ] 57 | * ] 58 | * ]; 59 | * ``` 60 | */ 61 | 62 | return [ 63 | ]; -------------------------------------------------------------------------------- /tmpl/environments/index.php: -------------------------------------------------------------------------------- 1 | ' => [ 11 | * 'name' => '', 12 | * 'path' => '' 13 | * ] 14 | * ]; 15 | * ``` 16 | */ 17 | 18 | return [ 19 | 'dev' => [ 20 | 'name' => 'Development', 21 | 'path' => 'dev' 22 | ], 23 | 'prod' => [ 24 | 'name' => 'Production', 25 | 'path' => 'prod', 26 | ], 27 | ]; 28 | -------------------------------------------------------------------------------- /tmpl/environments/prod/config.php: -------------------------------------------------------------------------------- 1 | 'NFR-123-456', 9 | * 10 | * // Modules to be installed. 11 | * // Warning: install the modules using DB migration. Install the modules 12 | * // using the settings of the environment, only for dev environment. 13 | * 'modules' => [ 14 | * 'vendor.debug' 15 | * ], 16 | * 17 | * // Options for modules 18 | * 'options' => [ 19 | * 'vendor.module' => [ 20 | * 'OPTION_CODE' => 'value', 21 | * 'OPTION_CODE' => ['value' => 'test', 'siteId' => 's1'] 22 | * ], 23 | * ], 24 | * 25 | * // Settings for module "cluster" 26 | * 'cluster' => [ 27 | * 'memcache' => [ 28 | * [ 29 | * 'GROUP_ID' => 1, 30 | * 'HOST' => 'host', 31 | * 'PORT' => 'port', 32 | * 'WEIGHT' => 'weight', 33 | * 'STATUS' => 'status', 34 | * ], 35 | * [ 36 | * 'GROUP_ID' => 1, 37 | * 'HOST' => 'host', 38 | * 'PORT' => 'port', 39 | * 'WEIGHT' => 'weight', 40 | * 'STATUS' => 'status', 41 | * ] 42 | * ] 43 | * ], 44 | * 45 | * // Values for file .settings.php 46 | * 'settings' => [ 47 | * 'connections' => [ 48 | * 'default' => [ 49 | * 'host' => 'host', 50 | * 'database' => 'db', 51 | * 'login' => 'login', 52 | * 'password' => 'pass', 53 | * 'className' => '\\Bitrix\\Main\\DB\\MysqlConnection', 54 | * 'options' => 2, 55 | * ], 56 | * ] 57 | * ] 58 | * ]; 59 | * ``` 60 | */ 61 | 62 | return [ 63 | ]; --------------------------------------------------------------------------------