├── LICENSE ├── README.md ├── bin └── bitrixoa ├── composer.json ├── docs └── bxrouter.md └── src ├── BitrixOaConfig.php ├── BitrixUiController.php ├── BitrixUiNativeController.php ├── UiPage.php └── UiService.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Вебпрактик 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 | # [bitrixoa] Bitrix OpenApi 2 | Пакет для генерации [Swagger UI](https://swagger.io/tools/swagger-ui/) на основе аннотаций при работе с [контроллерами](https://dev.1c-bitrix.ru/learning/course/index.php?COURSE_ID=43&LESSON_ID=6436&LESSON_PATH=3913.3516.5062.3750.6436) и роутером Bitrix. 3 | 4 | ## Установка 5 | ```angular2html 6 | composer install webpractik/bitrixoa 7 | ``` 8 | 9 | ## Генерация 10 | ```./vendor/bin/bitrixoa``` 11 | 12 | ### Параметры 13 | 1. `--bitrix-generate` параметр указывает, что openapi необходимо смотреть в директорию local/modules 14 | 2. `--index-mode` создаст сгенерированный /api-doc/index.php с разметкой swaggerui физически. 15 | 16 | ## Режимы работы 17 | ### A. Через нативный bitrix router (v20+) 18 | 19 | Если Ваш роутер не настроен, то прочтите [Настройка роутера Bitrix](docs/bxrouter.md): 20 | 1. Добавьте в роутер 21 | ```php 22 | use Bitrix\Main\Routing\RoutingConfigurator; 23 | 24 | return function (RoutingConfigurator $configurator) { 25 | $configurator->get('api-doc', [\BitrixOA\BitrixUiController::class, 'apidocAction']); 26 | }; 27 | ``` 28 | 2. В таком случае документация откроется по адресу `/api-doc` 29 | 30 | ### B. Через Bitrix Controller без роутера 31 | 1. Создайте в своем модуле файл `.settings.php` 32 | 2. Задайте корректный namespace и конфигурации для своего модуля 33 | 3. Скопируйте содержимое класса BitrixUiNativeController из этого пакета к себе в модуль, в свой класс-контроллер 34 | 4. Обращайтесь по адресу `<адрес сайта>/bitrix/services/main/ajax.php?action=<ваши настройки>` 35 | 36 | ### С. Статический UI 37 | Запустить генерацию с флагом `--index-mode` создаст сгенерированный `/api-doc/index.php` с разметкой swaggerui физически. 38 | 39 | ## Roadmap 40 | - [ ] Сделать генерацию путей на основе анализа роутера 41 | - [ ] Покрыть тестами 42 | -------------------------------------------------------------------------------- /bin/bitrixoa: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | false, 15 | 'format' => 'auto', 16 | 'exclude' => [], 17 | 'pattern' => '*.php', 18 | 'bootstrap' => false, 19 | 'help' => false, 20 | 'debug' => false, 21 | 'bitrix-generate' => false, 22 | 'index-mode' => false, 23 | 'processor' => [], 24 | ]; 25 | $aliases = [ 26 | 'o' => 'output', 27 | 'e' => 'exclude', 28 | 'n' => 'pattern', 29 | 'b' => 'bootstrap', 30 | 'h' => 'help', 31 | 'd' => 'debug', 32 | 'p' => 'processor', 33 | 'f' => 'format' 34 | ]; 35 | $needsArgument = [ 36 | 'output', 37 | 'format', 38 | 'exclude', 39 | 'pattern', 40 | 'bootstrap', 41 | 'processor', 42 | ]; 43 | $paths = []; 44 | $error = false; 45 | $bitrixMarker = false; 46 | $indexMarker = false; 47 | define('OpenApi\COLOR_RED', "\033[31m"); 48 | define('OpenApi\COLOR_YELLOW', "\033[33m"); 49 | define('OpenApi\COLOR_STOP', "\033[0m"); 50 | 51 | try { 52 | // Parse cli arguments 53 | for ($i = 1; $i < $argc; $i++) { 54 | $arg = $argv[$i]; 55 | if (substr($arg, 0, 2) === '--') { // longopt 56 | $option = substr($arg, 2); 57 | } elseif ($arg[0] === '-') { // shortopt 58 | if (array_key_exists(substr($arg, 1), $aliases)) { 59 | $option = $aliases[$arg[1]]; 60 | } else { 61 | throw new Exception('Unknown option: "' . $arg . '"'); 62 | } 63 | } else { 64 | $paths[] = $arg; 65 | continue; 66 | } 67 | if (array_key_exists($option, $options) === false) { 68 | throw new Exception('Unknown option: "' . $arg . '"'); 69 | } 70 | if (in_array($option, $needsArgument)) { 71 | if (empty($argv[$i + 1]) || $argv[$i + 1][0] === '-') { 72 | throw new Exception('Missing argument for "' . $arg . '"'); 73 | } 74 | if (is_array($options[$option])) { 75 | $options[$option][] = $argv[$i + 1]; 76 | } else { 77 | $options[$option] = $argv[$i + 1]; 78 | } 79 | $i++; 80 | } else { 81 | $options[$option] = true; 82 | if ($options['bitrix-generate']) { 83 | $bitrixMarker = true; 84 | } 85 | 86 | if ($options['index-mode']) { 87 | $indexMarker = true; 88 | } 89 | } 90 | } 91 | } catch (Exception $e) { 92 | $error = $e->getMessage(); 93 | } 94 | 95 | if ($bitrixMarker) { 96 | $paths = ['local/modules/']; 97 | } 98 | 99 | if (!$error && $options['bootstrap']) { 100 | if (is_readable($options['bootstrap']) === false) { 101 | $error = 'Invalid `--bootstrap` value: "'.$options['bootstrap'].'"'; 102 | } else { 103 | require_once($options['bootstrap']); 104 | } 105 | } 106 | if (count($paths) === 0) { 107 | $error = 'Specify at least one path.'; 108 | } 109 | if ($options['help'] === false && $error) { 110 | error_log(''); 111 | error_log(COLOR_RED.'Error: '.$error.COLOR_STOP); 112 | $options['help'] = true; // Show help 113 | } 114 | if ($options['help']) { 115 | $help = << 'Error', 150 | E_WARNING => 'Warning', 151 | E_PARSE => 'Parser error', 152 | E_NOTICE => 'Notice', 153 | E_STRICT => 'Strict', 154 | E_DEPRECATED => 'Deprecated', 155 | E_CORE_ERROR => 'Error(Core)', 156 | E_CORE_WARNING => 'Warning(Core)', 157 | E_COMPILE_ERROR => 'Error(compile)', 158 | E_COMPILE_WARNING => 'Warning(Compile)', 159 | E_RECOVERABLE_ERROR => 'Error(Recoverable)', 160 | E_USER_ERROR => 'Error', 161 | E_USER_WARNING => 'Warning', 162 | E_USER_NOTICE => 'Notice', 163 | E_USER_DEPRECATED => 'Deprecated', 164 | ]; 165 | set_error_handler(function ($errno, $errstr, $file, $line) use ($errorTypes, $options) { 166 | if (!(error_reporting() & $errno)) { 167 | return; // This error code is not included in error_reporting 168 | } 169 | $type = array_key_exists($errno, $errorTypes) ? $errorTypes[$errno] : 'Error'; 170 | $color = (substr($type, 0, 5) === 'Error') ? COLOR_RED: COLOR_YELLOW; 171 | error_log(COLOR_RED.$type. ': '.$errstr.COLOR_STOP); 172 | if ($options['debug']) { 173 | error_log(' in '.$file.' on line '.$line); 174 | } 175 | if (substr($type, 0, 5) === 'Error') { 176 | exit($errno); 177 | } 178 | }); 179 | set_exception_handler(function ($exception) use ($options) { 180 | if ($options['debug']) { 181 | error_log($exception); 182 | } else { 183 | error_log(COLOR_RED.'Exception: '.$exception->getMessage().COLOR_STOP); 184 | // if ($options['debug']) { 185 | // error_log(' in '.$exception->getFile().' on line '.$exception->getLine()); 186 | // } 187 | } 188 | exit($exception->getCode() ?: 1); 189 | }); 190 | $exit = 0; 191 | Logger::getInstance()->log = function ($entry, $type) use ($options, &$exit) { 192 | $exit = 1; 193 | if ($type === E_USER_NOTICE) { 194 | $type = ''; 195 | $color = COLOR_YELLOW; 196 | } else { 197 | $type = 'Warning: '; 198 | $color = COLOR_RED; 199 | } 200 | if ($entry instanceof Exception) { 201 | error_log(COLOR_RED."Error: " . $entry->getMessage().COLOR_STOP); 202 | if ($options['debug']) { 203 | error_log('Stack trace:'.PHP_EOL.$entry->getTraceAsString()); 204 | } 205 | } else { 206 | error_log($color. $type . $entry.COLOR_STOP); 207 | if ($options['debug']) { 208 | // Show backtrace in debug mode 209 | $e = (string)(new Exception('trace')); 210 | $trace = explode("\n", substr($e, strpos($e, 'Stack trace:'))); 211 | foreach ($trace as $i => $entry) { 212 | if ($i === 0) { 213 | error_log($entry); 214 | } 215 | if ($i <= 3) { 216 | continue; 217 | } 218 | preg_match('/#([0-9]+) (.*)$/', $entry, $match); 219 | error_log('#' .($match[1] - 2).' '.$match[2]); 220 | } 221 | } 222 | } 223 | }; 224 | $exclude = null; 225 | if ($options['exclude']) { 226 | $exclude = $options['exclude']; 227 | if (strpos($exclude[0], ',') !== false) { 228 | $exploded = explode(',', $exclude[0]); 229 | error_log(COLOR_RED.'Comma-separated exclude paths are deprecated, use multiple --exclude statements: --exclude '.$exploded[0].' --exclude '.$exploded[1]).COLOR_STOP; 230 | $exclude[0] = array_shift($exploded); 231 | $exclude = array_merge($exclude, $exploded); 232 | } 233 | } 234 | 235 | $pattern = "*.php"; 236 | if ($options['pattern']) { 237 | $pattern = $options['pattern']; 238 | } 239 | 240 | foreach ($options["processor"] as $processor) { 241 | $class = '\OpenApi\Processors\\'.$processor; 242 | if (class_exists($class)) { 243 | $processor = new $class(); 244 | } elseif (class_exists($processor)) { 245 | $processor = new $processor(); 246 | } 247 | Analysis::registerProcessor($processor); 248 | } 249 | 250 | $openapi = OpenApi\scan($paths, ['exclude' => $exclude, 'pattern' => $pattern]); 251 | 252 | if ($exit !== 0) { 253 | error_log(''); 254 | } 255 | if ($bitrixMarker) { 256 | $options['output'] = 'local/bitrixoa.yaml'; 257 | $openapi->saveAs($options['output'], $options['format']); 258 | } else { 259 | if ($options['output'] === false) { 260 | if (strtolower($options['format']) === 'json') { 261 | echo $openapi->toJson(); 262 | } else { 263 | echo $openapi->toYaml(); 264 | } 265 | echo "\n"; 266 | } else { 267 | if (is_dir($options['output'])) { 268 | $options['output'] .= '/bitrixoa.yaml'; 269 | } 270 | $openapi->saveAs($options['output'], $options['format']); 271 | } 272 | } 273 | 274 | 275 | if ($indexMarker) { 276 | (new \BitrixOA\UiService($options['output']))->exportWithIndexPage(); 277 | } 278 | 279 | exit($exit); 280 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpractik/bitrixoa", 3 | "description": "A package for generating annotations and drawing SwUi when working with Bitrix controllers.", 4 | "keywords": ["bitrix", "swagger", "bitrix controllers"], 5 | "bin": [ 6 | "bin/bitrixoa" 7 | ], 8 | "homepage": "https://github.com/webpractik/bitrixoa", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Петр Кленкин (mnlght)", 13 | "email": "pk@webpractik.ru" 14 | } 15 | ], 16 | "config": { 17 | "bin-dir": "bin", 18 | "sort-packages": true 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "BitrixOA\\": "src" 23 | } 24 | }, 25 | "require": { 26 | "zircote/swagger-php": "^3.1 || ^4.6.1" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/bxrouter.md: -------------------------------------------------------------------------------- 1 | # Инструкция по настройке роутера Bitrix 2 | 3 | 1. Внутри файла /bitrix/.settings.php добавьте секцию 4 | ```php 5 | 'routing' => 6 | array ( 7 | 'value' => 8 | array ( 9 | 'config' => 10 | array ( 11 | 'custom_routes.php' 12 | ), 13 | ), 14 | 'readonly' => true, 15 | ), 16 | ``` 17 | 2. Внутри папки bitrix/routes добавьте файл custom_routes.php с содержимым: 18 | ```php 19 | route = Application::getInstance()->getCurrentRoute(); 19 | parent::__construct($request); 20 | } 21 | 22 | public function getRoute(): Route 23 | { 24 | return $this->route; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/BitrixUiNativeController.php: -------------------------------------------------------------------------------- 1 | getHtml(); 31 | 32 | $response = new HttpResponse(); 33 | $response->setContent($swaggerPage); 34 | return $response; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/UiPage.php: -------------------------------------------------------------------------------- 1 | swaggerUiBlank = ' 12 | 13 | 14 | 15 | 16 | Swagger UI 17 | 18 | 19 | 20 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | 64 | 65 | '; 66 | } 67 | 68 | public function getHtml() 69 | { 70 | return $this->swaggerUiBlank; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/UiService.php: -------------------------------------------------------------------------------- 1 | uiPage = new UiPage($yamlPath); 14 | } 15 | 16 | /** 17 | * @throws \Exception 18 | */ 19 | public function exportWithIndexPage() 20 | { 21 | if (!is_dir('local')) { 22 | throw new Exception('В месте вызова не существует директория local'); 23 | } 24 | 25 | try { 26 | mkdir('api-doc'); 27 | $fp = fopen('api-doc/index.php', 'w'); 28 | fwrite($fp, $this->uiPage->getHtml()); 29 | fclose($fp); 30 | 31 | } catch (Exception $exception) { 32 | echo 'Во время экспорта документации произошла ошибка'; 33 | echo $exception->getMessage(); 34 | echo $exception->getTraceAsString(); 35 | } 36 | } 37 | 38 | public function exportWithBXRouteMode() 39 | { 40 | 41 | } 42 | } 43 | --------------------------------------------------------------------------------