Интересные задачи верстки и клиентского программирования

Битрикс24 приложение на Symfony

добавил Шубин Александр 17 Февраль, 2019


Была задача реализовать собственное действие в бизнес процессе битрикс24. Именно в облаке. В этом случае единственный вариант — написать собственное приложение и подключить его к нужному порталу. Никаких других вариантов кастомизации не предусмотрено, так как прямой доступ к серверному коду облака закрыт. В этой статье я обозначу основные шаги по реализации такого приложения. От новой пустой папки и до работающего финального действия в бизнес процессе.

Для тех кому проще изучить код без пояснений, смотрите пример в репозитории на гитхабе — ссылка. Пояснения о том что именно и зачем было сделано читайте в статье. Поехали 🙂

Установка symfony

Мы начнем прям совсем с базовой версии symfony — скелета. Чтобы в приложении не было ничего лишнего. Открываем папку с проектами и в ней выполняем:

$ composer create-project symfony/skeleton b24project

После выполнения кода (если вы в курсе что такое композер, и окружение соответствует требованиям пакета) будет создана папка b24project. Можно сразу открыть эту папку в IDE. В дальнейшем на протяжении всей статьи мы будем работать именно внутри этой папки.

Дополнительные пакеты

Голый фреймворк может отвечать на запросы и не больше. Поэтому нам в любом случае понадобятся дополнительные пакеты. Но мы сами подберем конфигурацию этих пакетов. Давайте посмотрим что нам понадобится для работы приложения битрикс24.

Doctrine. Работа с базой

Наше приложение должно где-то хранить данные, поэтому оно должно уметь работать с базой данных. Для работы с БД один из самых удобных вариантов это Doctrine ORM. Установим пакет с доктриной:

$ composer require symfony/orm-pack

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

$ composer require symfony/maker-bundle --dev

С помощью этого пакета мы будем быстро создавать файлики с описанием таблиц в БД (файлики сущностей).

Кроме того, давайте сразу укажем данные для доступа к серверу БД. Откройте файл /.env и отредактируйте там строку DATABASE_URL=

DATABASE_URL=драйвер://логин_пользователя:пароль_пользователя@адрес_сервера:3306/база_даных

//то есть реально должно получиться что-то вроде этого
DATABASE_URL=mysqli://testapp:ge4fweTTf@127.0.0.1:3306/testapp
  • Драйвер — это расширение с помощью которого php будет работать с базой. На php7 для работы с mysql используется расширение mysqli, с i на конце
  • Логин пользователя — собственно логин
  • Пароль пользователя — собственно пароль
  • Адрес сервера — можете прописать localhost, или любой другой внешний домен или ip адрес
  • База данных — уже созданная база к которой пользователь должен иметь полные права

Twig. Формирование HTML

Для symfony де факто стандартом является движок шаблонизации twig. В нем мы будем формировать значительную часть html нашего приложения, например HTML для страницы настроек приложения на стороне Б24 будет формироваться именно твигом. Для установки twig выполняем команду:

$ composer require twig

В корне проекта добавится новая папка /templates в которой и будут лежать все наши шаблоны.

Webpack Encore. Работа со стилями и скриптами

Помимо html для нашего приложения понадобятся стили и скрипты. Можно просто положить их куда то в /public, например в /public/css и /public/js, а потом напрямую подключать в html. Это нормальный вариант, и если не хочется заморачиваться или мало опыта в сборке фронта то возможно стоит его рассмотреть.

Можно самостоятельно настроить систему сборки фронтенда на gulp, webpack и т.д., по какой то команде билдить нужные файлы в /public и точно так же подключать в html просто тэгом script. Если у вас есть понимание как работает webpack, то это на мой взгляд лучший вариант.

Если же у вас нет опыта работы со сборкой фронта, то можете воспользоваться webpack encore. Это ни что иное как синтаксический сахар написанный поверх webpack. На выходе он формирует обычный конфиг для вебпака. Кроме того encore создает файлик /public/build/entrypoints.json в котором автоматически появляются точки входа в наше приложение, и в шаблонах мы будем писать не просто тэг script, а спец. функцию, которая прочтет файл entrypoints.json и подключит нужные файлы в шаблоне самостоятельно.

Для баланса простоты и функциональности все примеры в данной статье будут с использованием webpack encore. Webpack encore ставится с помощью менеджера пакетов yarn. Если у вас есть только голая нода (node.js), то для установки выполните следующие команды:

//установим yarn глобально
$ npm install -g yarn

//пакет композера, плюс стартовые package.json и webpack.config.js
$ composer require encore

//Ставим npm пакеты из package.json
$ yarn install

Yarn это менеджер пакетов, такой же как npm если что. После выполнения вышеперечисленных команд в корне проекта появятся три новых файла package.json со списком пакетов ноды, webpack.config.js в котором уже настроена работа encore, yarn.lock аналог composer.lock для yarn. Кроме того добавится новая папка /assets именно в ней мы будем писать стили и скрипты. В процессе билда файлы из /assets преобразуются и выкладываются в /public/build.

Подробно на конфигурировании вебпака в этой статье я не буду останавливаться. Один из несомненных плюсов его использования — возможность писать современный js код и транспилировать его в старый формат с использованием babel. Помимо этого при использовании вебпака вы так же можете использовать sass, less, stylus, postcss и т.д. для понятного и нормально поддерживаемого css кода. Для современных веб приложений это просто обязательные условия. Использование вебпака открывает возможность без боли использовать фронтенд фреймворки — такие как vue, react, angular.

В общем если у вас планируется более менее значимый интерфейс в приложении, то крайне советую подключить webpack или webpack encore. Если же большая часть функционала будет без интерфейса, то хватит и просто пары файликов в /public.

Monolog. Логгирование

По требованию битрикса все запросы которые мы отправляем в б24 нужно логировать и держать лог минимум три дня. Но даже без этого требования логирование запросов будет нам полезно. Поэтому мы поставим monolog.

$ composer require symfony/monolog-bundle

В процессе установки будут созданы файлы конфигов, в частности для dev окружения будет создан /config/packages/dev/monolog.yaml. Именно в нем описывается где будут храниться логи (для dev окружения). По дефолту все логи приложения будут складываться в файл /var/log/dev.log

Bitrix24 SDK

Максим Месилов сделал хорошую библиотеку для серверных запросов. Не забудьте сказать ему спасибо если будете пользоваться пакетом. Спасибо Максим 🙂 Вот ссылка https://github.com/mesilov/bitrix24-php-sdk. Давайте установим пакет с библиотекой. Выполняем в консоли:

$ composer require mesilov/bitrix24-php-sdk

Для более сложных, чем получение пользователя запросов нам понадобится client_id и client_secret приложения. Вы можете посмотреть их в списке установленных приложений, или на странице редактирования приложения (подробнее в разделе HelloWorld на Битрикс24). Давайте сохраним эти данные в /.env файле, рядом с доступами для базы данных. Добавляем в конец файла три строки:

B24_APPLICATION_SCOPE=user,bizproc
B24_APPLICATION_ID=local.5c6c5aa4f3ee58.91761697
B24_APPLICATION_SECRET=HhucTmSa9mOiH5byaSmoEVsQLi4PbY1Bxsy4Txy5Haq1e3ecXd
  • B24_APPLICATION_SCOPE — это список разрешений приложения, какие галки вы поставили. В моем случае я поставил две галки, бизнес процессы и пользователи. Указываем тут идентификаторы этих разрешений через запятую.
  • B24_APPLICATION_ID — это client_id со страницы приложения на портале. При создании тиражного решения эта переменная называется application_id, именно поэтому в библиотеке она так названа
  • B24_APPLICATION_SECRET — это client_secret со страницы списка установленных приложений

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

Контроль версий

Теперь, когда мы поставили все пакеты которые нам понадобятся добавим весь код нашего проекта в систему контроля версий. Чтобы если что-то пойдет не так всегда можно было откатиться на чистую установку. Выполняем в корне проекта:

$ git init
$ git add *
$ git commit -m "Symfony installed"

После выполнения этих трех команд весь проект за исключением папок /vendor, /var и файлов /.env, /.gitignore будет добавлен в контроль версий и в любой момент можно будет откатиться на сохраненный вариант. Файлы /.env и /.gitignore добавляйте в контроль версий по желанию.

Итог установки

Итого на текущий момент у нас есть папка b24project внутри которой установлена symfony в минимальной комплектации. К минимальной комплектации мы добавили пакеты doctrine, twig, encore, monolog, bitrix24 sdk. Можете какие то пакеты не ставить, например если работа с базой не нужна, то доктрину исключаем. Вебпак кажется слишком сложным или js вообще не планируется, исключаем пакет с encore. Приложение работает только по api и без интерфейса вообще? Исключаем twig и т.д.

В общем ни один из этих пакетов не обязателен. Только вам принимать решение. Сам по себе скелет очень прост, это не система управления заводом. По сути просто несколько базовых правил куда и какие файлики складывать. Но эти правила реально удобны.

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

Настраиваем процесс разработки

Подготовительная работа по установке почти закончена. Однако же приложение у нас есть только локально, из внешнего интернета его никто не видит. Но для интеграции с облачным битрикс24 наше приложение должно отправлять и принимать запросы. Битрикс24 никак не сможет достучаться до нашего локального компьютера, поэтому нам понадобится отдельный сервер на каком то публичном хостинге с поддержкой PHP.

О том как настраивать веб-сервер я рассказывать не буду. Отмечу только что корнем веб сервера должна быть папка /public из корня проекта. То есть если в браузере мы наберем https://example.com/index.php то должен отработать файл /public/index.php. Сам корень проекта из браузера доступен быть не должен. О том какие конфиги для апача или nginx нужны для работы symfony можете прочитать по ссылке. Без корректного конфига у вас не заработает роутер, то есть по сути будет работать только главная страница сайта, а все остальные будут отдавать 404.

Наш домен должен быть доступен по протоколу https. То есть прям у вас в браузере должен открываться https://example.com. Рекомендую сразу настроить для домена let’s encrypt SSL сертификат. Чтобы ваш браузер не ругался при открытии фрейма с приложением на стороне битрикс24.

После настройки сервера закачайте вообще все файлы проекта, кроме /node_modules, /var и /vendor в папку сайта. Далее авторизуйтесь на сервере по ssh и выполните в корне:

$ composer install

В результате выполнения команды на сервере появится папка vendor, причем в точности такая же как и у вас локально. Данные о версиях пакетов будут получены из composer.lock.

Попробуйте открыть ваш публичный домен в браузере. Если все сделано верно, то в браузере вы увидите вот такую страницу:

страница успешной установки symfony

Синхронизация файлов

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

Если вы ведете разработку в PHPStorm, то можете включить автоматическую отправку всех измененных файлов на сервер по умолчанию. Подробнее на скрине:

настройка автоматической загрузки изменений в phpstorm

Обратите внимание на строку «Upload changed files automatically to the default server». Если вы настроили deplyment сервер (это делается прям рядом, на уровень выше в меню), то любой измененный локально файл попадет на публичный сервер.

Если вы хотите чтобы даже поставленные локально пакеты композера и npm автоматически загружались на сервер, то не ставьте галку «Skip external changes». Я рекомендую оставить эту галку пустой. Да, не очень приятно ждать иногда, но пакеты композера это как правило мегабайты, а не сотни мегабайт. Ничего катастрофического не случится если они зальются на сервер.

Таким образом после включения автоматической закачки, в процессе разработки создается полная иллюзия того, что вы пишете код прямо на сервере. Поправили что-то локально и прям тут же по F5 можете открыть публичный сайт в браузере и увидеть изменения.

Пишем «Hello World!» на Symfony

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

Контроллер

Заставим наше приложение отобразить хоть что-то в браузере. Для этого создаем файл /src/Controller/DefaultController.php, который будет содержать следующий код:

<?php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

class DefaultController extends AbstractController
{
    public function settings()
    {
        return $this->render('settings.html.twig');
    }
}

Просто пустой класс контроллера который должен отрендерить файл settings.html.twig.

Шаблон

Давайте создадим файл /templates/settings.html.twig который мы рендерим в контроллере выше

{% extends 'base.html.twig' %}

{% block title %}Bitrix Settings{% endblock %}

{% block body %}
    <h2>Настройки приложения</h2>
    Hello world!
{% endblock %}

В шаблоне мы наследуемся от базового файла /templates/base.html.twig и переопределяем два блока title и body исходного шаблона. То есть на выходе получится тот же самый базовый шаблон, но вместо блоков title и body у нас будет текст который мы переопределили

URL

У нас есть контроллер и шаблон. Остается заставить symfony показать нам этот шаблон по какому либо адресу с помощью контроллера. Для этого отредактируем файл /config/routes.yaml

settings:
    path: /settings
    controller: App\Controller\DefaultController::settings

Таким образом мы говорим symfony, что по адресу example.com/settings должен отработать метод settings() контроллера App\Controller\DefaultController

Сразу после того как добавите маршрут, в браузере, по адресу ваш_домен/settings должна открыться вот такая страница:

первая страница hello world

Скрипты и стили

Теперь давайте добавим серый фон на эту страницу. Если решили писать код стилей и скриптов сразу в /public, то этот раздел можете пропустить. Тут я покажу как работает сборка в webpack encore. Отредактируем четыре файла:

  • Создаем файл /assets/css/settings.css
body {
    background-color: #858585;
}
  • Создаем файл /assets/js/settings.js
require('../css/settings.css');

alert('Hello world!');
  • В файле /webpack.config.js добавим новую входную точку нашего приложения

Файл большой, но там в основном комментарии. Так что не пугайтесь. По сравнению с дефолтным конфигом мы добавляем только одну строку .addEntry(‘settings’, ‘./assets/js/settings.js’), она под номером 21 ниже:

var Encore = require('@symfony/webpack-encore');

Encore
    // directory where compiled assets will be stored
    .setOutputPath('public/build/')
    // public path used by the web server to access the output path
    .setPublicPath('/build')
    // only needed for CDN's or sub-directory deploy
    //.setManifestKeyPrefix('build/')

    /*
     * ENTRY CONFIG
     *
     * Add 1 entry for each "page" of your app
     * (including one that's included on every page - e.g. "app")
     *
     * Each entry will result in one JavaScript file (e.g. app.js)
     * and one CSS file (e.g. app.css) if you JavaScript imports CSS.
     */
    .addEntry('app', './assets/js/app.js')
    .addEntry('settings', './assets/js/settings.js')
    //.addEntry('page1', './assets/js/page1.js')
    //.addEntry('page2', './assets/js/page2.js')

    // will require an extra script tag for runtime.js
    // but, you probably want this, unless you're building a single-page app
    .enableSingleRuntimeChunk()

    /*
     * FEATURE CONFIG
     *
     * Enable & configure other features below. For a full
     * list of features, see:
     * https://symfony.com/doc/current/frontend.html#adding-more-features
     */
    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    // enables hashed filenames (e.g. app.abc123.css)
    .enableVersioning(Encore.isProduction())

    // enables Sass/SCSS support
    //.enableSassLoader()

    // uncomment if you use TypeScript
    //.enableTypeScriptLoader()

    // uncomment if you're having problems with a jQuery plugin
    //.autoProvidejQuery()

    // uncomment if you use API Platform Admin (composer req api-admin)
    //.enableReactPreset()
    //.addEntry('admin', './assets/js/admin.js')
;

module.exports = Encore.getWebpackConfig();

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

  • И в конце редактируем наш шаблон /templates/settings.html.twig
{% extends 'base.html.twig' %}

{% block stylesheets %}
    {{ parent() }}
    {{ encore_entry_link_tags('settings') }}
{% endblock %}
{% block javascripts %}
    {{ parent() }}
    {{ encore_entry_script_tags('settings') }}
{% endblock %}

{% block title %}Bitrix Settings{% endblock %}
{% block body %}
    <h2>Настройки приложения</h2>
    Hello world!
{% endblock %}

То есть добавляем два новых блока stylesheets и javascripts. Функция encore_entry_link_tags(‘settings’) при рендеринге шаблона подставит обычные тэги link для всех стилей из точки входа settings. А функция encore_entry_script_tags(‘settings’) подставит обычные тэги script для скриптов из входной точки.

Остается сбилдить наш фронт командой:

$ yarn encore prod

Если вы теперь откроете страницу ваш_домен/settings, то увидите такое:

серая страница hello world

Фон стал серым, в точности как мы обозначили в нашем файле стилей. Плюс при открытии страницы отработал алерт из файла скриптов.

Каждый раз при изменении файлов билдить фронт не очень удобно. Поэтому если вы активно пишете скрипты или стили, то запустите webpack encore в режиме отслеживания изменений:

$ yarn encore dev --watch

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

Пишем «Hello world!» на Битрикс24

Теперь когда в целом понятно как вообще обрабатывать входящие запросы на symfony, я покажу как открыть эту серую страницу внутри нашего портала. Как и во всех курсах битрикса выведем имя пользователя который просматривает эту страницу.

Для начала создадим приложение:

Добавление приложение в облаке битрикс24

Кликаем по кнопке «Добавить» слева. Приложение будет только для личного использования. Откроется страница добавления приложения:

Я все лишнее убрал. Заполняем только три поля и ставим одну галку:

  1. Поле «Название приложения» (любой текст)
  2. Поле «Название пункта меню» (любой текст)
  3. Поле «Укажите ссылку». Прописываем ссылку на наше публичное приложение, сразу в раздел /settings
  4. Ставим галку «Пользователи (user)» в блоке «Права доступа»

После этого нажимаем сохранить и переходим на страницу приложения. Если все сделано правильно, то вы увидите серую страницу:

Страница настроек приложения на портале

Имя пользователя на странице настроек

В данном случае рассмотрим более простой вариант с клиентским скриптом. Напишем немного js кода. В шаблоне /templates/settings.html.twig подключим клиентскую библиотеку и обозначим место для вывода имени пользователя:

{% extends 'base.html.twig' %}

{% block stylesheets %}
    {{ parent() }}
    {{ encore_entry_link_tags('settings') }}
{% endblock %}
{% block javascripts %}
    {{ parent() }}
    <script src="//api.bitrix24.com/api/v1/"></script>
    {{ encore_entry_script_tags('settings') }}
{% endblock %}

{% block title %}Bitrix Settings{% endblock %}
{% block body %}
    <h2>Настройки приложения</h2>
    Hello world!

    <h3>Привет <span id="username"></span>!</h3>
{% endblock %}

Тут добавились две строки. Подключение скрипта в блоке javascripts и тэг h3 внутри которого мы выведем ФИО пользователя.

Напишем клиентский код с запросом к rest api в файле /assets/js/settings.js и убираем наш тестовый алерт:

require('../css/settings.css');

BX24.init(function(){

    BX24.callMethod('user.current', {}, function(res){
        var name = document.getElementById('username');
        name.innerHTML = res.data().NAME + ' ' + res.data().LAST_NAME;
        console.log(res.data());
    });

});

Билдим фронт командой:

$ yarn encore prod

После чего открываем наше приложение на портале битрикс24. На всякий случай напоминаю, билд выполнился локально, если вы не настроили автоматическую загрузку измененных файлов, то вам нужно вручную загрузить папку /public/build на публичный сервер. В приложении если все сделано правильно вы увидите вот это:

корректный результат использования клиентской js библиотеки

Если кратко, то при открытии страницы настроек приложения была загружена официальная библиотека для работы с рест апи, она лежит по адресу api.bitrix24.com/api/v1/, после того как библиотека скачалась пользователю в браузер, прямо из браузера пользователя выполнился аякс запрос к порталу — user.current, запрос вернул данные и в браузере с помощью прямой манипуляции с HTML кодом мы подставили ФИО текущего пользователя в span с id=username.

В консоли браузера вы можете посмотреть какие именно данные нам отправляет портал битрикс24 в ответ на наш запрос:

смотрим содержимое ответа битрикс24

Если переключиться на вкладку preview или response внутри конкретного запроса, то будет видно собственно сами данные пользователя. На скрине не стал переключать, потому что там моя рабочая почта.

Процесс установки приложения

По клиентской библиотеке понятно. Мы просто подключили и пользуемся, о том как там работает авторизация и т.д. даже не задумываемся, все это уже встроено в библиотеку. Однако же если мы хотим взаимодействовать с порталом без пользователя вообще, в фоновом режиме, то разобраться с авторизацией все же придется.

Типы приложений битрикс24

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

Я бы разделил все виды приложений на клиентские и серверные. Клиентские содержат только js код и стили. А серверные получают и отправляют запросы с какого то внешнего по отношению к порталу сервера.

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

Проверяем данные при установке

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

Добавляем новый маршрут в файле /config/routes.yaml

settings:
    path: /settings
    controller: App\Controller\DefaultController::settings

install:
    path: /install
    controller: App\Controller\DefaultController::install

То есть приложение у нас уже будет отвечать по двум адресам — https://example.com/settings и https://example.com/install.

Добавим новый метод install() в наш класс дефолтного контроллера, в котором будем логгировать все переменные из строки запроса (get, query) и тела запроса (post):

<?php
namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends AbstractController
{
    public function settings()
    {
        return $this->render('settings.html.twig');
    }

    public function install(LoggerInterface $logger, Request $request)
    {
        $logger->info('Install request', [
            'post' => $request->request->all(),
            'query' => $request->query->all()
        ]);

        return new Response('Install test');
    }
}

Никаких дополнительных настроек делать не нужно. Symfony самостоятельно поймет что нашему контроллеру первым аргументом нужен логгер и вторым аргументом объект запроса. Порядок аргументов и их содержимое symfony подставит самостоятельно. В качестве теста откройте страницу https://example.com/install на публичном сервере и убедитесь, что в файле /var/log/dev.log данные вашего запроса сохранились, а на экране в браузере вы увидели «Install test».

Теперь удалим наше приложение Hello World на стороне битрикс24 и добавим в точности так же, только плюсом еще заполним поле «Укажите ссылку для первоначальной установки (необязательно)». После чего открываем страницу приложения. Видим следующую картину:

страница установки приложения

То есть до тех пор пока приложение не скажет что оно корректно установилось, вместо страницы настроек всегда будет отображаться страница установки.

Смотрим лог в файле /var/log/dev.log. Оказывается битрикс в процессе установки присылает нам следующие данные:

"post": {
    "AUTH_ID": "1bb8655c0036366000357ad200000001201c033e963e75681e45b775bef4d569d12cca",
    "AUTH_EXPIRES": "3600",
    "REFRESH_ID": "0b378d5c0036366000357ad200000001201c03830d217f4fda83ca7d7947f423e25aae",
    "member_id": "ad10d230b7b77292cee0aba4f512c219",
    "status": "L",
    "PLACEMENT": "DEFAULT"
  },
  "query": {
    "DOMAIN": "b24-jyhe5e.bitrix24.ru",
    "PROTOCOL": "1",
    "LANG": "ru",
    "APP_SID": "2a6fa452ff756621baf19fc9803cb061"
  }

Сохраним эти данные для дальнейшей автономной работы приложения.

Таблица с данными авторизации

Для хранения данных нам понадобится таблица в БД. Для начала создадим файл сущности и потом уже на публичном сервере по файлу сущности сгенерируем таблицу в базе. Выполняем в консоли:

$ php bin/console make:entity

Эта команда запустит мастер по генерации файла сущности. В процессе генерации укажите имя сущности Portal. Укажите поля и их типы domain — string, access_token — string, refresh_token — string, member_id — string, expires — datetime, changed — datetime.

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

По завершению мастера будут созданы файлы /src/Entity/Portal.php и /src/Repository/PortalRepository.php. Пока что нам важен только первый. Заходим на публичный сервер по ssh и выполняем там команду:

$ php bin/console doctrine:schema:create

Если при установке доктрины вы корректно прописали данные для доступа к БД, то в результате выполнения команды вы увидите сообщение «Database schema created successfully!». И в phpmyadmin будет такая структура:

структура таблицы portal в приложении

Сохраняем данные авторизации

Теперь когда у нас есть таблица мы прямо в процессе установки можем взять нужные данные и сохранить их в БД. В этом нам поможет класс PortalRepository. Добавляем функцию получения объекта по member_id в файле /src/Repository/PortalRepository.php:

<?php

namespace App\Repository;

use App\Entity\Portal;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Symfony\Bridge\Doctrine\RegistryInterface;

/**
 * @method Portal|null find($id, $lockMode = null, $lockVersion = null)
 * @method Portal|null findOneBy(array $criteria, array $orderBy = null)
 * @method Portal[]    findAll()
 * @method Portal[]    findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
 */
class PortalRepository extends ServiceEntityRepository
{
    public function __construct(RegistryInterface $registry)
    {
        parent::__construct($registry, Portal::class);
    }

    public function findOneByMemberId($member_id): Portal
    {
        $portal = $this->createQueryBuilder('p')
            ->andWhere('p.member_id = :val')
            ->setParameter('val', $member_id)
            ->getQuery()
            ->getOneOrNullResult();

        if($portal === null) {
            $portal = new Portal();
        }

        return $portal;
    }

}

И давайте сразу воспользуемся этой новой функцией findOneByMemberId() в нашем контроллере:

<?php

namespace App\Controller;

use App\Entity\Portal;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends AbstractController
{
    public function settings()
    {
        return $this->render('settings.html.twig');
    }

    public function install(Request $request, EntityManagerInterface $em)
    {
        $message = 'success';
        $errorsList = [];
        try {

            $repository = $em->getRepository(Portal::class);
            $portal = $repository->findOneByMemberId($request->get('member_id'));

            $portal->setDomain($request->get('DOMAIN'))
                ->setAccessToken($request->get('AUTH_ID'))
                ->setRefreshToken($request->get('REFRESH_ID'))
                ->setMemberId($request->get('member_id'));

            //setting changed date
            $changed = new \DateTime('now');
            $portal->setChanged($changed);

            //setting expires date
            $expires = clone $changed;
            $duration = (int)$request->get('AUTH_EXPIRES');
            $expires->add(new \DateInterval('PT' . $duration . 'S'));
            $portal->setExpires($expires);

            //saving
            $em->persist($portal);
            $em->flush();

        } catch (\Exception $exception) {
            $message = 'failed';
            $errorsList[] = $exception->getMessage();
        }

        return new Response('Install ' . $message . '. ' . implode(', ', $errorsList));
    }
}

Пробуем открыть страницу приложения на портале, видим сообщение «Install success», либо «Install failed» и список ошибок. Если вы все сделали правильно, то должно быть сообщение об успешной установке.

Если кратко суть происходящего пересказать, то в контроллере установки мы получем объект $portal. Репозиторий нам вернет существующий объект, если для этого member_id уже есть запись, либо просто новый чистый объект если это первый запрос на установку. Далее в контроллере мы заполняем объект $portal данными из запроса и сохраняем в БД.

Таким образом мы в процессе установки сохранили данные авторизации для последующего автономного использования.

Завершение установки

Как я уже говорил выше, вместо страницы настроек приложения у нас будет отображаться страница установки приложения до тех пор, пока мы не скажем битриксу, что все прошло нормально. Собственно давайте это и скажем 🙂

Делаем новую входную точку (entry) для webpack encore, пусть она будет называться install. Самостоятельно добавьте новую строку в файле webpack.config.js.

Далее копируем шаблон settings.html.twig в install.html.twig и редактируем его:

{% extends 'base.html.twig' %}

{% block stylesheets %}
    {{ parent() }}
    {{ encore_entry_link_tags('install') }}
{% endblock %}
{% block javascripts %}
    {{ parent() }}
    <script src="//api.bitrix24.com/api/v1/"></script>
    {{ encore_entry_script_tags('install') }}
{% endblock %}

{% block title %}Bitrix Settings{% endblock %}
{% block body %}
    <h2>Установка приложения</h2>

    {% if errorsList %}
        <div id="result" class="failed">
            Ошибки при установке приложения:
            <ul>
                {% for error in errorsList %}
                    <li>{{ error }}</li>
                {% endfor %}
            </ul>
        </div>
    {% else %}
        <div id="result" class="success">
            Приложение успешно установлено
        </div>
    {% endif %}

{% endblock %}

В файле /assets/js/install.js вызываем метод с запросом об успешной установке:

require('../css/install.css');

BX24.init(function () {
    var result = document.getElementById("result");
    if(result.className === 'success') {
        BX24.installFinish();
    }
});

Ну и наконец в контроллере install() заменяем прямой Response() на функцию рендеринга шаблона:

return $this->render('install.html.twig', ['errorsList' => $errorsList]);

Билдим фронт. Откроем страницу приложения на портале в последний раз. Если все нормально, на короткое время там промелькнет сообщение «Приложение успешно установлено» и откроется серая страница настроек приложения.

Запрос к порталу от Symfony

Воспользуемся сохраненными в процессе установки токенами для запросов к порталу без открытия страницы приложения. Мы будем открывать обычную страницу на нашем публичном сервере, где средствами PHP и расширения curl отправим запрос к порталу. Клиентскую библиотеку использовать не будем.

Добавляем новый маршрут example.com/test в файле /config/routes.yaml:

settings:
    path: /settings
    controller: App\Controller\DefaultController::settings

install:
    path: /install
    controller: App\Controller\DefaultController::install

test:
    path: /test
    controller: App\Controller\DefaultController::test

И добавляем новый метод test() в файл с дефолтным контроллером. В этом методе просто сделаем запрос на чистом PHP:

public function test(EntityManagerInterface $em)
{
    $repository = $em->getRepository(Portal::class);
    $portal = $repository->find(1);

    $queryUrl = 'https://' . $portal->getDomain() . '/rest/user.current.json';
    $queryData = http_build_query([
        'auth' => $portal->getAccessToken()
    ]);

    $curl = curl_init();
    curl_setopt_array($curl, [
        CURLOPT_POST => 1,
        CURLOPT_HEADER => 0,
        CURLOPT_RETURNTRANSFER => 1,
        CURLOPT_URL => $queryUrl,
        CURLOPT_POSTFIELDS => $queryData,
    ]);

    $result = json_decode(curl_exec($curl), true);
    curl_close($curl);

    return new Response('<pre>' . print_r($result, true) . '</pre>');
}

Так как в таблице у нас пока что одна запись, то мы ее получаем просто по ID. При открытии страницы example.com/test вы увидите такое:

результат запроса в браузере

Код будет работать только какое то определенное время. А потом токен авторизации устареет, и будет отображаться ошибка. Поэтому токен необходимо продлять.

Автоматическое обновление accessToken

Мы уже понемногу приближаемся к написанию бизнес логики нашего приложения. Для написания бизнес логики давайте создадим папку /src/Bitrix и весь код для взаимодействия с порталом битрикс24 будем писать в этой папке. Начнем с ядра приложения. Создадим файл /src/Bitrix/B24App.php со следующим содержимым:

<?php

namespace App\Bitrix;

use App\Entity\Portal;
use Bitrix24\Bitrix24;
use Bitrix24\Exceptions\Bitrix24Exception;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;

class B24App extends Bitrix24
{
    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;

    /**
     * @var Portal
     */
    protected $portal;

    public function __construct($isSaveRawResponse = false, ?LoggerInterface $logger = null)
    {
        parent::__construct($isSaveRawResponse, $logger);

        $this->setApplicationScope(explode(',', $_ENV['B24_APPLICATION_SCOPE']));
        $this->setApplicationId($_ENV['B24_APPLICATION_ID']);
        $this->setApplicationSecret($_ENV['B24_APPLICATION_SECRET']);

        $this->setOnExpiredToken([$this, 'refreshAccessToken']);
    }

    public function call($methodName, array $additionalParameters = [])
    {
        if ($this->portal === null) {
            $this->fetchAccessToken();
        }

        return parent::call($methodName, $additionalParameters);
    }

    public function fetchAccessToken(): void
    {
        $repository = $this->entityManager->getRepository(Portal::class);
        $this->portal = $repository->find(1);

        if ($this->portal === null) {
            throw new \RuntimeException('No access token found');
        }

        $this->setDomain($this->portal->getDomain());
        $this->setMemberId($this->portal->getMemberId());
        $this->setAccessToken($this->portal->getAccessToken());
        $this->setRefreshToken($this->portal->getRefreshToken());

        if ($this->portal->getExpires() < new \DateTime('now')) {
            $this->refreshAccessToken();
        }
    }

    protected function refreshAccessToken(): void
    {
        $this->setRedirectUri('https://' . $_ENV['APP_DOMAIN'] . '/install');
        $result = $this->getNewAccessToken();

        if ($result['member_id'] === $this->portal->getMemberId()) {
            $userData = [
                'access_token' => $result['access_token'],
                'refresh_token' => $result['refresh_token'],
            ];
            $this->persistPortalData($userData);

            //saving for current run
            $this->setAccessToken($this->portal->getAccessToken());
            $this->setRefreshToken($this->portal->getRefreshToken());
        } else {
            throw new Bitrix24Exception('Wrong member_id given');
        }
    }

    public function addNewPortal(array $data): void
    {
        if (isset(
            $data['domain'],
            $data['access_token'],
            $data['refresh_token'],
            $data['member_id'])
        ) {
            $repository = $this->entityManager->getRepository(Portal::class);
            $this->portal = $repository->findOneByMemberId($data['member_id']);
            $this->persistPortalData($data);
        }
    }

    public function persistPortalData(array $data): void
    {
        if (isset($data['domain'])) {
            $this->portal->setDomain($data['domain']);
        }

        if (isset($data['access_token'])) {
            $this->portal->setAccessToken($data['access_token']);
        }

        if (isset($data['refresh_token'])) {
            $this->portal->setRefreshToken($data['refresh_token']);
        }

        if (isset($data['member_id'])) {
            $this->portal->setRefreshToken($data['member_id']);
        }

        //setting changed date
        $changed = new \DateTime('now');
        $this->portal->setChanged($changed);

        //setting expires date
        $expires = clone $changed;
        $duration = 3600;
        if (isset($data['expires_in']) && (int)$data['expires_in'] < 1) {
            $duration = (int)$data['expires_in'];
        }
        $expires->add(new \DateInterval('PT' . $duration . 'S'));
        $this->portal->setExpires($expires);

        $this->entityManager->persist($this->portal);
        $this->entityManager->flush();
    }

    /**
     * @param EntityManagerInterface $entityManager
     */
    public function setEntityManager(EntityManagerInterface $entityManager): void
    {
        $this->entityManager = $entityManager;
    }
}

Тут мы расширяем класс Bitrix24 из Bitrix SDK. Если пропустили, то вернитесь в начало статьи и посмотрите как устанавливается библиотека. Ядром нашего приложения будет именно этот расширенный класс. Он будет отвечать за автоматическое продление токена, за регистрацию новых пользователей, за установку параметров приложения. Пройдемся по его методам:

  • Метод __construct() — в этом методе мы вызываем родительский конструктор, а так же сразу ставим основные параметры приложения из переменных окружения. Плюс регистрируем обработчик, который вызовется при устаревании токена.
  • Метод call() — вообще все вызовы в SDK используют именно метод call() для запросов к порталу. Таким образом при первом же запросе мы проверяем наличие свойства $portal, если этого свойства нет, то очевидно и токены еще не установлены. Для установки вызывается метод fetchAccessToken()
  • Метод fetchAccessToken() — в этом методе мы получаем токен пользователя из базы и устанавливаем его в приложении. В конце метода проверяется срок действия токена, если он уже устарел, то сразу же вызывается refreshAccessToken()
  • Метод refreshAccessToken() — в этом методе мы делаем запрос к серверу битрикса, чтобы он выдал нам новый accessToken взамен устаревшего. Новый accessToken сразу сохраняется в базу через persistPortalData()
  • Метод addNewPortal() — этот метод нужен для вызова при установке приложения. Весь код который работал с БД из контроллера install() мы удаляем, и оставляем только вызов этого метода. Внутри он так же получает объект $portal по member_id и сохраняет данные авторизации через persistPortalData()
  • Метод persistPortalData() — метод из входящего массива достает соответствующие ключи и ставит их в объект $portal. А потом просто сохраняет $portal в базе данных.

Помимо обозначенных выше методов не описанным остался еще один — setEntityManager(). Этот метод устанавливает свойство $entityManager в нашем классе приложения. Однако же явно он нигде не вызывается. Эта функция вызывается через Dependency Injection, сразу при создании экземпляра класса. Поэтому давайте дадим знать symfony про наш метод, так как автоматически он не подхватится. Редактируем файл /config/services.yaml, а именно блок services:

services:
    # default configuration for services in *this* file
    _defaults:
        autowire: true      # Automatically injects dependencies in your services.
        autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.

    # makes classes in src/ available to be used as services
    # this creates a service per class whose id is the fully-qualified class name
    App\:
        resource: '../src/*'
        exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'

    # controllers are imported separately to make sure services can be injected
    # as action arguments even if you don't extend any base controller class
    App\Controller\:
        resource: '../src/Controller'
        tags: ['controller.service_arguments']

    App\Bitrix\B24App:
        calls:
            - [setEntityManager]

По сравнению с дефолтным файлом тут добавились три строки в самом конце. Эти три строки заставят symfony автоматически вызвать метод setEntityManager() при неявном создании экземпляра класса B24App.

Первый запрос на SDK

Чтобы продемонстрировать как будет работать наш класс B24App давайте опять же получим данные по текущему пользователю. Изменяем метод test() в дефолтном контроллере на такое:

public function test(B24App $b24App)
    {
        $user = new \Bitrix24\User\User($b24App);
        $result = $user->current();

        return new Response('<pre>' . print_r($result, true) . '</pre>');
    }

И пробуем открыть example.com/test в браузере. У вас должен отобразиться массив с данными по пользователю портала который ставил это приложение (так как именно его данными для авторизации мы пользуемся). При этом прошлая реализация метода test() через какое то время начинала выдавать ошибку, однако же наша новая реализация ошибку не вызовет никогда.

В момент выполнения запроса user.current внутри B24App происходит получение accessToken из базы данных (при этом если ни одного запроса к порталу не было отправлено, то и базу мы дергать не будем). Тут же проверяется его срок действия и если он вышел, то еще до отправки запроса на получение пользователя приложение запросит у битрикса новый, свежий токен.

Мы не дожидаемся ошибки об устаревании токена при выполнении запроса, и смотрим время жизни по нашим локальным данным приложения. Однако же если вдруг у нас локально хранятся некорректные данные, то битрикс нам вернет ошибку, и в этот момент выполнится обработчик setOnExpiredToken() который мы выставляли в конструкторе B24App.

Таким образом у нас есть двойная защита от старых токенов. Локальная проверка, плюс обработчик если все же запрос вернул ошибку.

Действия приложений в бизнес процессе

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

Добавление действия БП

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

Как писать запрос на клиентской библиотеке объяснялось в курсе обучения по бизнес процессам — ссылка. Можно добавить кнопку на страницу настроек и обработчик клика по этой кнопке, в обработчике отправить запрос на добавление действия. В этой статье давайте напишем серверный запрос, который отправится при открытии example.com/test. В нашем дефолтном контроллере переписываем метод test():

public function test(B24App $b24App)
{
    $result = [];
    try {
        $user = new \Bitrix24\User\User($b24App);
        $userResponse = $user->current();

        $activityData = [
            'CODE' => 'helloWorld', // символьный код нашего действия
            'HANDLER' => 'https://' . $_ENV['APP_DOMAIN'] . '/activities/test',// скрипт-обработчик действия
            'AUTH_USER_ID' => $userResponse['result']['ID'], // ID пользователя, токен которого будет передан приложению.
            'USE_SUBSCRIPTION' => '',// Использование подписки. Допустимые значения - Y или N.
            'NAME' => [
                'ru' => 'Hello World!' // название действия в редакторе БП
            ],
            'DESCRIPTION' => [
                'ru' => 'Тестовое действие' // описание действия в редакторе БП
            ],
            'PROPERTIES' => [// массив входных параметров
                'document_id' => [
                    'Name' => [
                        'ru' => 'ID документа'
                    ],
                    'Description' => [
                        'ru' => 'ID текущего документа'
                    ],
                    'Type' => 'int',
                    'Required' => 'Y',
                    'Multiple' => 'N',
                    'Default' => '',
                ]
            ],
            'RETURN_PROPERTIES' => [// массив выходных параметров
                'message' => [
                    'Name' => [
                        'ru' => 'Сообщение из приложения'
                    ],
                    'Type' => 'text',
                    'Multiple' => 'N',
                    'Default' => null
                ],
            ]
        ];
        $result = $b24App->call('bizproc.activity.add', $activityData);
    } catch (\Exception $exception) {
        $result['error'] = $exception->getMessage();
    }

    return new Response('<pre>' . print_r($result, true) . '</pre>');
}

В коде большую часть места занимает описание полей. Само действие это всего лишь один вызов $b24App->call().

При первом открытии страницы example.com/test отобразится корректный результат добавления:

Корректный результат добавления действия БП серверным запросом

При втором и всех последующих открытиях будет отображаться ошибка:

Ошибка добавления, действие уже существует

Если после корректного добавления действия открыть дизайнер бизнес процессов, то мы увидим наше действие:

Действие hello world в дизайнере бизнес процессов

Обработка действия БП

Что вообще делает действие приложения в бизнес процессе? Если на суть происходящего посмотреть.

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

Таким образом для обработки действия на стороне приложения мы должны получить данные бизнес процесса во входящем запросе, а потом отправить внешний запрос из приложения к порталу.

Зарегистрируем новый адрес по которому будет отвечать наше приложение. Редактируем файл /config/routes.yaml:

settings:
    path: /settings
    controller: App\Controller\DefaultController::settings

install:
    path: /install
    controller: App\Controller\DefaultController::install

test:
    path: /test
    controller: App\Controller\DefaultController::test

activity_test:
    path: /activities/test
    controller: App\Controller\ActivityController::test

Мы добавили четвертый адрес по которому будет отвечать наше приложение. Тут мы используем новый контроллер ActivityController. Поэтому скопируем наш дефолтный класс контроллера в соседний файл. Редактируем /stc/Controller/ActivityController.php:

<?php

namespace App\Controller;

use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class ActivityController extends AbstractController
{
    public function test(LoggerInterface $logger, Request $request): Response
    {
        $logger->info('Activity test', [
            'post' => $request->request->all(),
            'query' => $request->query->all()
        ]);

        return new Response('Ok');
    }
}

Посмотрим что битрикс отправляет нам в исходящем запросе с портала. Метод тест содержит только логирование входящих данных и больше ничего. Как выясняется после отработки действия нам приходят следующие данные:

{
  "post": {
    "workflow_id": "5c680c61983e95.01100872",
    "code": "helloWorld",
    "document_id": [
      "crm",
      "CCrmDocumentDeal",
      "DEAL_1"
    ],
    "document_type": [
      "crm",
      "CCrmDocumentDeal",
      "DEAL"
    ],
    "event_token": "5c680c61983e95.01100872|A8831_19009_85155_29961|3frrwJ54E3yRj113PABk931r6tmep8gn.012d81c5bbc96845b91a6c87e68c990bd0c1e98968391d18428f94cd28ccc6ef",
    "properties": {
      "document_id": "1"
    },
    "use_subscription": "Y",
    "timeout_duration": "36000",
    "ts": "1550322785",
    "auth": {
      "access_token": "711a685c00363ba000357ad200000001000000de0a3b2f19ee09ff70b0edcdb51a10b6",
      "expires": "1550326385",
      "expires_in": "3600",
      "scope": "bizproc,user",
      "domain": "b24-jyhe5e.bitrix24.ru",
      "server_endpoint": "https://oauth.bitrix.info/rest/",
      "status": "L",
      "client_endpoint": "https://b24-jyhe5e.bitrix24.ru/rest/",
      "member_id": "ad10d230b7b77292cee0aba4f512c219",
      "user_id": "1",
      "refresh_token": "61998f5c00363ba000357ad200000001000000a0981884298f7bc8834621327d943345",
      "application_token": "35b6e3f35e5cd953d52edc93a664bcf1"
    }
  },
  "query": []
}

Данных довольно много. И все они в post. В строке запроса не приходит ничего. Из важных особенностей стоит обратить внимание на accessToken и refreshToken. Мы можем взять данные авторизации для исходящего запроса прямо тут, из данных входящего запроса. Оставить наш токен из базы данных в покое.

На этот входящий запрос, приложение должно отправить исходящий запрос. Формат исходящего запроса описан тут — ссылка. Давайте в этом же методе test() нашего контроллера сразу отправим исходящий запрос. Переписываем метод test() в ActivityController:

<?php

namespace App\Controller;

use App\Bitrix\B24App;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class ActivityController extends AbstractController
{
    public function test(B24App $b24App, Request $request): Response
    {
        $message = 'Hello world! Обрабатываем документ - ' . $request->get('properties')['document_id'];

        $requestData = [
            'event_token' => $request->get('event_token'),
            'return_values' => [
                'message' => $message
            ]
        ];

        $b24App->call('bizproc.event.send', $requestData);

        return new Response('Ok');
    }
}

То есть прям сразу при получении запроса от битрикс24 мы на этом же хите отправляем новый запрос с данными приложения. В исходящем от приложения запросе мы передаем переменную message. Эту переменную мы регистрировали в RETURN_PROPERTIES при добавлении действия приложения.

Сам бизнес процесс у нас выглядит так:

простой бизнес процесс с уведомлением

В настройках уведомления выбираем переменную «Hello World! …Сообщение из приложения»:

Список переменных из приложения в редакторе БП

Если мы делаем бизнес процесс для сделки, то ставим галку «Автоматически запускать: при изменении» и пробуем перетащить сделку между статусами. Если сам бизнес процесс мы настроили верно, то в чате увидим такое:

Уведомление в чате сгенерированное бизнес процессом

То есть наше приложение получило входящий запрос от портала, тут же отправило исходящий запрос с переменной message. Бизнес процесс продолжил работу и отправил пользователю уведомление со значением из переменной message, которую наше приложение вернуло на портал. Приложение может отправлять любой текст, и именно этот текст уйдет в уведомлении пользователю.

В общем то все работает.

Далее можно сделать отдельную таблицу для обработки запросов от приложения, и отправлять результат не на том же хите, а по крону. Чтобы точно знать, что все данные обработаны корректно. Авто продление токенов так же можно повесить на крон, на случай если приложение используется не очень активно. Раз в пару недель выполнять задачу, в которой вызывать метод refreshAccessToken() или просто делать запрос на получение пользователя.

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

Подведем итоги

Из этой статьи вы должны были понять как установить symfony с нуля в минимальной комплектации, но с нужным именно вам функционалом — ничего лишнего. А так же принципы взаимодействия портала Битрикс24 и приложения на стороннем сервере, в том числе суть происходящего в процессе авторизации — что вообще за токены туда-сюда пуляются, куда и с каким содержимым приходят запросы.

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

Ну и как обычно, если информация показалась вам полезной, то скажите спасибо в комментариях. Лайк, шер, все дела. Это единственное что меня мотивирует писать чаще 🙂


добавил Шубин Александр 17 Февраль, 2019
Рубрика: Bitrix24


Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *