D7-аналоги любимых функций в 1С-Битрикс

Анатолий Ерофеев

Эта статья уникальна.

С одной стороны – это самая популярная страница нашего посещаемого и насыщенного информацией сайта. Несколько сотен человек каждый день читают ее.

С другой – автор опубликовал важнейшую для мира Битрикс-разработки справочную информацию за полгода до появления официальной документации. Новизна, качество и системность изложения дают плоды.

Почему так получилось? Мы отличаемся от "просто веб-разработчиков на Битриксе".

ИНТЕРВОЛГА – универсальный компетентный веб-интегратор. Мы можем решить все задачи интеграции собственными силами. Все наши проекты – комплексные, интеграционные. Мы работаем только проектными командами и не можем браться за "мелкий ремонт" поломок и багов. Подробный принцип нашей работы описан в статье «Как мы работаем в больших проектах».

Мы приносим пользу бизнесу клиента за счет осмысленного применения веб-технологий.

Важная составляющая такого подхода – уникальные, высококлассные и мотивированные на хорошую работу разработчики. Один из таких – Анатолий Ерофеев, автор статьи.

А теперь собственно прекрасная статья Анатолия Ерофеева

Новое ядро D7 в 1С-Битрикс: Управление сайтом решительно замещает старое. Все чаще использование привычных методов и классов приводит к предупреждению от IDE “Method/class is deprecated”. Предлагаю “знать врага в лицо” и провести небольшой обзор таких классов D7, которые уже сейчас можно и нужно использовать, чтобы не прослыть в среде разработчиков неотесанным неандертальцем. Но замечу, что ядро D7 – это не просто рефакторинг, это смена подхода к написанию кода.

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

Подключение стилей и скриптов

CMain::AddHeadScript, CMain::SetAdditionalCss, CMain::AddHeadString
Давным-давно в далекой-далекой версии Битрикса разработчики вставляли стили и скрипты шаблона в документ банальными тегами <script> и <link>. Потом в фаворе оказались названные выше отложенные функции. Сейчас устарели и они. Нынче на дворе 2016 год и подключать надо так: // Old school
$APPLICATION->AddHeadScript(SITE_TEMPLATE_PATH . "/js/fix.js");
$APPLICATION->SetAdditionalCSS(SITE_TEMPLATE_PATH . "/styles/fix.css");
$APPLICATION->AddHeadString("<link href='http://fonts.googleapis.com/css?family=PT+Sans:400&subset=cyrillic' rel='stylesheet' type='text/css'>");

// D7
use Bitrix\Main\Page\Asset;

Asset::getInstance()->addJs(SITE_TEMPLATE_PATH . "/js/fix.js");
Asset::getInstance()->addCss(SITE_TEMPLATE_PATH . "/styles/fix.css");
Asset::getInstance()->addString("<link href='http://fonts.googleapis.com/css?family=PT+Sans:400&subset=cyrillic' rel='stylesheet' type='text/css'>");
Эти методы динамические, а класс реализует паттерн “одиночка” (Singletone) и обратиться к объекту можно через Bitrix\Main\Page\Asset::getInstance().

По слухам, этот класс может быть в скором времени переписан, так что необходимо держать руку на пульсе.

Срочно в номер: на днях была опубликована документация по новому способу подключения JS и CSS в шаблонах компонентов.

Подключение модулей

CModule::IncludeModule, CModule::IncludeModuleEx

Уже многие выучили, что вместо старого доброго CModule для подключения модулей нужно применять новый бодрый Bitrix\Main\Loader.
// Old school
CModule::IncludeModule("iblock");
CModule::IncludeModuleEx("intervolga.tips");

// D7
use Bitrix\Main\Loader;

Loader::includeModule("iblock");
Loader::includeSharewareModule("intervolga.tips");

Локализация

GetMessage, IncludeModuleLangFile, IncludeTemplateLangFile

Чтобы быть стильным-модным-молодежным рекомендуется пользоваться следующим кодом для обращения к языковым файлам и переменным:

// Old school
IncludeTemplateLangFile(__FILE__);
echo GetMessage("INTERVOLGA_TIPS.TITLE");

// D7
use Bitrix\Main\Localization\Loc;

Loc::loadMessages(__FILE__);
echo Loc::getMessage("INTERVOLGA_TIPS.TITLE");

Настройки модулей

COption::SetOptionInt, COption::SetOptionString, COption::GetOptionInt, COption::GetOptionString, COption::RemoveOption

Претерпел изменения и код для работы с чтением и записью настроек модулей. На смену классу COption пришел Bitrix\Main\Config\Option:

// Old school
COption::SetOptionString("main", "max_file_size", "1024");
$size = COption::GetOptionInt("main", "max_file_size");
COption::RemoveOption("main", "max_file_size", "s2");

// D7
use Bitrix\Main\Config\Option;

Option::set("main", "max_file_size", "1024");
$size = Option::get("main", "max_file_size");
Option::delete("main", array(
    "name" => "max_file_size",
    "site_id" => "s2"
    )
);

Пропало разделение методов на int и string, а при удалении теперь используется массив-фильтр.

В новом классе есть несколько совершенно новых методов, не имеющих “старых” аналогов, так что самостоятельное изучение исходного кода приветствуется.

Кеширование

CPHPCache::StartDataCache, CPHPCache::InitCache, CPHPCache::GetVars, CPHPCache::EndDataCache, CPHPCache::AbortDataCache

Спасибо Андрею Загальскому за пример работы с кешированием в ядре D7. Новый класс Bitrix\Main\Data\Cache почти ничем не отличается в обращении от старого CPHPCache.

// Old school
$cache = new CPHPCache();
if ($cache->InitCache($cacheTime, $cacheId, $cacheDir))
{
    $result = $cache->GetVars();
}
elseif($cache->StartDataCache())
{
    $result = array();
    // ...
    if ($isInvalid)
    {
        $cache->AbortDataCache();
    }
    // ...
    $cache->EndDataCache($result);
}

// D7
$cache = Bitrix\Main\Data\Cache::createInstance();
if ($cache->initCache($cacheTime, $cacheId, $cacheDir))
{
    $result = $cache->getVars();
}
elseif ($cache->startDataCache())
{
    $result = array();
    // ...
    if ($isInvalid)
    {
        $cache->abortDataCache();
    }
    // ...
    $cache->endDataCache($result);
}

События

AddEventHandler, RemoveEventHandler, RegisterModuleDependences, UnRegisterModuleDependences, GetModuleEvents

Теперь за кратко- и долгосрочную регистрацию обработчиков событий отвечает один класс: Bitrix\Main\EventManager.

// Old school
$handler = AddEventHandler("main",
    "OnUserLoginExternal",
    array(
        "Intervolga\\Test\\EventHandlers\\Main",
        "onUserLoginExternal"
    )
);
RemoveEventHandler(
    "main",
    "OnUserLoginExternal",
    $handler
);
RegisterModuleDependences(
    "main",
    "OnProlog",
    $this->MODULE_ID,
    "Intervolga\\Test\\EventHandlers",
    "onProlog"
);
UnRegisterModuleDependences(
    "main",
    "OnProlog",
    $this->MODULE_ID,
    "Intervolga\\Test\\EventHandlers",
    "onProlog"
);

$handlers = GetModuleEvents("main", "OnProlog", true);

// D7
use Bitrix\Main\EventManager;

$handler = EventManager::getInstance()->addEventHandler(
    "main",
    "OnUserLoginExternal",
    array(
        "Intervolga\\Test\\EventHandlers\\Main",
        "onUserLoginExternal"
    )
);
EventManager::getInstance()->removeEventHandler(
    "main",
    "OnUserLoginExternal",
    $handler
);
EventManager::getInstance()->registerEventHandler(
    "main",
    "OnProlog",
    $this->MODULE_ID,
    "Intervolga\\Test\\EventHandlers",
    "onProlog"
);
EventManager::getInstance()->unRegisterEventHandler(
    "main",
    "OnProlog",
    $this->MODULE_ID,
    "Intervolga\\Test\\EventHandlers",
    "onProlog"
);
$handlers = EventManager::getInstance()->findEventHandlers("main", "OnProlog");

Bitrix\Main\EventManager, так же как Bitrix\Main\Page\Asset, реализует паттерн Одиночка, обращаться к нему нужно через getInstance().

Важное замечание: в обработчики, зарегистрированные с помощью addEventHandler в качестве аргумента будет передан объект события (Bitrix\Main\Event). Если хотите, чтобы передавались старые добрые аргументы, нужно использоватьaddEventHandlerCompatible. Аналогично с registerEventHandler и registerEventHandlerCompatible.

Файловая структура

CheckDirPath, DeleteDirFilesEx, RewriteFile

Тут нынче раздолье для ООП-программиста. Для работы с файлами, папками и всем прочим – отдельные классы, все типизировано и напоминает Java. Самые “главные” классы здесь – Bitrix\Main\IO\Directory и Bitrix\Main\IO\File (ну и немного Bitrix\Main\IO\Path).

// Old school
CheckDirPath($_SERVER["DOCUMENT_ROOT"] . "/foo/bar/baz/");
RewriteFile(
    $_SERVER["DOCUMENT_ROOT"] . "/foo/bar/baz/1.txt",
    "hello from old school!"
);
DeleteDirFilesEx("/foo/bar/baz/");

// D7
use Bitrix\Main\Application;
use Bitrix\Main\IO\Directory;
use Bitrix\Main\IO\File;

Directory::createDirectory(
    Application::getDocumentRoot() . "/foo/bar/baz/"
);
File::putFileContents(
Application::getDocumentRoot() . "/foo/bar/baz/1.txt",
    "hello from D7"
);
Directory::deleteDirectory(
    Application::getDocumentRoot() . "/foo/bar/baz/"
);

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

Обратите внимание: вместо $_SERVER["DOCUMENT_ROOT"] сейчас можно использовать Bitrix\Main\Application::getDocumentRoot().

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

Исключения

CMain::ThrowException, CMain::ResetException, CMain::GetException

Открою небольшой секрет: в нашем дружном коллективе возник локальный холивар. Мы спорили, стоит ли помещать в эту статью раздел про исключения и обработку ошибок. Ведь тут не просто одни методы заменили другие – тут изменился подход (о чем я говорил в начале статьи), были переосмыслены некоторые базовые принципы многих классов. Есть две причины, почему этот раздел все-таки опубликован:
  1. Это важная тема, и говоря о переходе на D7 про нее нельзя не упомянуть
  2. Автор статьи все-таки я :P
Итак, одним из наиболее существенных изменений в D7 стала обработка ошибок при помощи механизма исключений (полное соблюдение религии исключений в php). Если происходит ошибка – выбрасываем исключение. Если хотим обработать ошибку – ловим исключение. Базовый класс для всех исключений в системе: Bitrix\Main\SystemException.

// Old school
global $APPLICATION;
$APPLICATION->ResetException();
$APPLICATION->ThrowException("Error");
//...
if ($exception = $APPLICATION->GetException())
{
    echo $exception->GetString();
}

// D7
use Bitrix\Main\SystemException;

try
{
    // ...
    throw new SystemException("Error");
}
catch (SystemException $exception)
{
    echo $exception->getMessage();
}

Отладка

AddMessage2Log, mydump

Ах, сколько раз выручал бывалого разработчика этот метод (AddMessage2Log). Неизменный товарищ при отладке ajax-запросов, крон-файлов и всего, невидимого глазу администратора. Но сегодня устарел и он, а на смену ему спешат два новых “молодца”:

// Old school
define("LOG_FILENAME", $_SERVER["DOCUMENT_ROOT"]."/bitrix/log-intervolga.txt");

AddMessage2Log($_SERVER);
echo "<pre>" . mydump($_SERVER) . "</pre>";

// D7
use Bitrix\Main\Diag\Debug;

Debug::dumpToFile($_SERVER);
// or
Debug::writeToFile($_SERVER);

Debug::dump($_SERVER);

Первый для любителей var_dump, второй для адептов print_r’а.

Так же в этом классе есть несколько совершенно новых методов, не имеющих аналогов в старом ядре. Например, методы для измерения времени:

// D7
use Bitrix\Main\Diag\Debug;
Debug::startTimeLabel("foo");
foo();
Debug::endTimeLabel("foo");

Debug::startTimeLabel("bar");
bar();
Debug::endTimeLabel("bar");
        
print_r(Debug::getTimeLabels());

Почтовые события

CEvent::Send, CEvent::SendImmediate

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

// Old school
CEvent::Send(
    "NEW_USER",
    "s1",
    array(
        "EMAIL" => "info@intervolga.ru",
        "USER_ID" => 42
    )
);

// D7
use Bitrix\Main\Mail\Event;
Event::send(array(
    "EVENT_NAME" => "NEW_USER",
    "LID" => "s1",
    "C_FIELDS" => array(
        "EMAIL" => "info@intervolga.ru",
        "USER_ID" => 42
    ),
));

Работа с GET- и POST-параметрами страницы

$_GET, $_POST, $_REQUEST
Ошибки использования $_POST
Чтобы избавиться от глобальных переменных в коде (суперглобальных в том числе) в D7 изобрели класс Bitrix\Main\HttpRequest. Забирать get- и post-переменные теперь можно через него.

// Old school
$name = $_POST["name"];
$email = htmlspecialchars($_GET["email"]);

// D7
use Bitrix\Main\Application;
$request = Application::getInstance()->getContext()->getRequest();

$name = $request->getPost("name");
$email = htmlspecialchars($request->getQuery("email"));

Конструировать объект самому не нужно, добраться до него можно через приложение и контекст (как в примере кода выше).

Работа с cookie

CMain::set_cookie, CMain::get_cookie

Класс CMain слишком много себе позволяет. Он и компоненты подключал, и CSS/JS регистрирует, и хлебные крошки собирает, и т.п. В этом разделе мы рассмотрим, куда “сбежали” методы работы с куками. В старом ядре было 2 метода: для создания и для получения кук. Теперь всё стало гораздо глубже: задавать куки нужно через класс “ответа сервера” – Bitrix\Main\HttpResponse, получать их нужно через класс “запроса к серверу” – Bitrix\Main\HttpRequest.

// Old school
global $APPLICATION;
$APPLICATION->set_cookie("TEST", 42, false, "/", "example.com");
// Cookie будет доступна только на следующем хите!
echo $APPLICATION->get_cookie("TEST");

// D7
use Bitrix\Main\Application;
use Bitrix\Main\Web\Cookie;
$cookie = new Cookie("TEST", 42);
$cookie->setDomain("example.com");
Application::getInstance()->getContext()->getResponse()->addCookie($cookie);
// Cookie будет доступна только на следующем хите!
echo Application::getInstance()->getContext()->getRequest()->getCookie("TEST");

Также работа с куками может вестись силами класса Bitrix\Main\Web\HttpClient, но это совсем другая история.

Важное замечание: запись куки, добавленной через D7, произойдет только при подключении эпилога (там вызывается метод Bitrix\Main\HttpResponse::flush())

Работа со ссылками

CMain::GetCurPageParam, DeleteParam

Для работы со ссылками в ядре D7 есть отдельный класс: Bitrix\Main\Web\Uri. Правда, работа с ним не так “компактна”, как с его предшественником. Но это лишь следствие того, что в этом классе соблюдается принцип “1 метод – 1 ответственность”.

// Old school
global $APPLICATION;
$redirect = $APPLICATION->GetCurPageParam("foo=bar", array("baz"));

// D7
use Bitrix\Main\Application;
use Bitrix\Main\Web\Uri;

$request = Application::getInstance()->getContext()->getRequest();
$uriString = $request->getRequestUri();
$uri = new Uri($uriString);
$uri->deleteParams(array("baz"));
$uri->addParams(array("foo" => "bar"));
$redirect = $uri->getUri();

ORM-классы

Их реально много. Их просто нужно “знать в лицо”, так как это будет основной инструмент на многие годы, как когда-то был CIBlockElement::GetList сотоварищи. В таблице в конце статьи я приведу основные ORM-классы, на которые стоит обратить внимание уже сегодня.

Запросы к БД

CDatabase::Query

Хоть это и не рекомендуется, но возможность поработать с БД прямыми запросами в продукте остается. Теперь доступ получается не через глобальный объект $DB класса CDatabase, а через Bitrix\Main\DB\Connection. “Добираться” до подключения нужно через Bitrix\Main\Application:

// Old school
global $DB;
$record = $DB->Query("select 1+1;")->Fetch();
AddMessage2Log($record);

// D7
use Bitrix\Main\Application;
use Bitrix\Main\Diag\Debug;

$record = Application::getConnection()
->query("select 1+1;")
->fetch();
Debug::writeToFile($record);

Хозяйке на заметку: больше про ORM и работу с БД в D7 можно почерпнуть из другой нашей статьи.

Итоговая таблица аналогов старых методов и классов в новом ядре D7 (с примерами вызова)

Было (старое ядро)

Стало (новое ядро D7)

CMain::AddHeadScript

Bitrix\Main\Page\Asset::addJs

CMain::SetAdditionalCss

Bitrix\Main\Page\Asset::addCss

CMain::AddHeadString

Bitrix\Main\Page\Asset::addString

CModule::IncludeModule

Bitrix\Main\Loader::includeModule

CModule::IncludeModuleEx

Bitrix\Main\Loader::includeSharewareModule

GetMessage

Bitrix\Main\Localization\Loc::getMessage

IncludeModuleLangFile, IncludeTemplateLangFile

Bitrix\Main\Localization\Loc::loadMessages

COption::SetOptionInt, COption::SetOptionString,

Bitrix\Main\Config\Option::set

COption::GetOptionInt, COption::GetOptionString

Bitrix\Main\Config\Option::get

COption::RemoveOption

Bitrix\Main\Config\Option::delete

CPHPCache::StartDataCache

Bitrix\Main\Data\Cache::startDataCache

CPHPCache::InitCache

Bitrix\Main\Data\Cache::initCache

CPHPCache::GetVars

Bitrix\Main\Data\Cache::getVars

CPHPCache::EndDataCache

Bitrix\Main\Data\Cache::endDataCache

CPHPCache::AbortDataCache

Bitrix\Main\Data\Cache::abortDataCache

AddEventHandler

Bitrix\Main\EventManager::addEventHandler (новый формат),
Bitrix\Main\EventManager::addEventHandlerCompatible

RemoveEventHandler

Bitrix\Main\EventManager::removeEventHandler (новый формат),
Bitrix\Main\EventManager::registerEventHandlerCompatible

RegisterModuleDependences

Bitrix\Main\EventManager::registerEventHandler

UnRegisterModuleDependences

Bitrix\Main\EventManager::unRegisterEventHandler

GetModuleEvents

Bitrix\Main\EventManager::findEventHandlers

CheckDirPath

Bitrix\Main\IO\Directory::createDirectory

DeleteDirFilesEx

Bitrix\Main\IO\Directory::deleteDirectory

RewriteFile

Bitrix\Main\IO\File::putFileContents

CMain::ThrowException

Bitrix\Main\SystemException

CMain::ResetException

Bitrix\Main\SystemException

CMain::GetException

Bitrix\Main\SystemException

AddMessage2Log

Bitrix\Main\Diag\Debug::dumpToFile,

Bitrix\Main\Diag\Debug::writeToFile

mydump

Bitrix\Main\Diag\Debug::dump

CEvent::Send

Bitrix\Main\Mail\Event::send

CEvent::SendImmediate

Bitrix\Main\Mail\Event::sendImmediate

$_REQUEST

Bitrix\Main\HttpRequest::get

$_GET

Bitrix\Main\HttpRequest::getQuery

$_POST

Bitrix\Main\HttpRequest::getPost

CMain::set_cookie

Bitrix\Main\HttpResponse::addCookie

CMain::get_cookie

Bitrix\Main\HttpRequest::getCookie

CMain::GetCurPageParam

Bitrix\Main\Web\Uri::addParams,

Bitrix\Main\Web\Uri::deleteParams,

Bitrix\Main\Web\Uri::getUri

DeleteParam

Bitrix\Main\Web\Uri::deleteParams,

Bitrix\Main\Web\Uri::getUri

CDatabase::Query

Bitrix\Main\DB\Connection::query

ORM-классы

Класс в старом ядре

Класс в новом ядре D7

Таблица БД

CUser

Bitrix\Main\UserTable

b_user

CFile

Bitrix\Main\FileTable

b_file

CGroup

Bitrix\Main\GroupTable

b_group

CSite

Bitrix\Main\SiteTable

b_lang

CIBlockElement

Bitrix\Iblock\ElementTable

b_iblock_element

CIBlock

Bitrix\Iblock\IblockTable

b_iblock

CIBlockProperty

Bitrix\Iblock\PropertyTable

b_iblock_property

CIBlockSection

Bitrix\Iblock\SectionTable

b_iblock_section

CIBlockPropertyEnum

Bitrix\Iblock\PropertyEnumerationTable

b_iblock_property_enum

CCatalogStore

Bitrix\Catalog\StoreTable

b_catalog_store

CCatalogProduct

Bitrix\Catalog\ProductTable

b_catalog_product

CCatalogGroup

Bitrix\Catalog\GroupTable

b_catalog_group

CSaleOrder

Bitrix\Sale\Internals\OrderTable

b_sale_order

CSaleBasket

Bitrix\Sale\Internals\BasketTable

b_sale_basket

CSaleOrderProps

Bitrix\Sale\Internals\OrderPropsTable

b_sale_order_props

CSaleOrderPropsValue

Bitrix\Sale\Internals\OrderPropsValueTable

b_sale_order_props_value

Выводы

Проблемы с использованием Битрикс D7 Складывается интересная ситуация. Компания 1С-Битрикс активно рекламирует новое ядро D7, на каждой конференции все чаще слышны фразы “компонент/модуль полностью переписан на новом ядре, классах и т.п.”, старый код все чаще отмечается как @deprecated. Но при всем при этом никакой документации нет. Документация посвящена только старому ядру и если нужно познакомиться с D7 – то нужно держать руку на пульсе событий: читать исходники, форумы, блоги, общаться с техподдержкой, изучать официальные видеокурсы( Разработка на D7.Введение и D7. Разработка собственного модуля). С одной стороны, это может повысить “порог вхождения” в “клуб программистов D7” и сделать их еще более бородатыми, с другой – может отпугнуть новичков.

Будем надеяться, что исчерпывающая документация по новому ядру все-таки появится и мы (программисты, пользователи и разработчики 1С-Битрикс: Управление сайтом) пойдем в светлое будущее, где уже маячат PHP 7, EcmaScript 6 и другие радости web-технологий.

P.S. спасибо Сергею Покоеву, Алексею Шкарупе и Степану Овчинникову за помощь в написании статьи.

UPD: спасибо Дмитрию Черноярову за дополнения по работе с $_GET, cookie и событиями.

UPD 2: на свет появилась документация по новому ядру d7!
Оцените статью
25.02.2016
Понравилась статья?
Поделитесь ссылкой с друзьями и коллегами!

Статьи по теме

07.03.2023
Дорожная карта внедрения платформы автоматизации оптовых продаж Про построение эффективных отделов продаж написано много крутых статей. Одни эксперты готовы сделать это за 10 шагов, другие предлагают многоэтапную эволюц...
16.02.2023
Как начать B2B-продажи онлайн - особенности и методы оптовой торговли После пандемии рынок e-commerce начал стремительно расти. Мы говорим не только о B2C, но и о B2B-сегменте. Многие крупные компании уже разглядели потенциал...
10.01.2023
Как битриксоиды в React уходили Приятно познакомиться, мы битриксоиды. Да-да, те самые которые: вообще не модные, пишут НЕ на Laravel и Symfony, ...
10.01.2023
Товарная дистрибуция 30 лет спустя. Как программисты изменили продажи крупного бизнеса «Я думал, что буду строить банк, а на самом деле построил ИТ-компанию» Олег Тиньков, безработный Есть такая штука — товарная дистри...
10.01.2023
Как мы решили выпускать собственный продукт через CustDev и у нас получилось Собственный продукт как фиксация компетенции&nbsp; В развитии крупных компаний-аутсорсеров наступает момент, когда они уже обросли опытом и компетенциями ...
19.12.2022
Учимся настраивать свою почту, не наступая на чужие грабли: Postfix + msmtp + сайт Привет, меня зовут Никита, я backend-разработчик в компании ИНТЕРВОЛГА. Работаю в компании уже 3 года, и за этот срок достаточно часто мне приходилось вози...

Мы работаем по одному из двух форматов:

  • аренда команды (от 2 человек, не менее 3 месяцев);
  • итерации с фиксированной ценой (1-3 месяца длительностью).

ИНТЕРВОЛГА предоставляет:

  • регулярные онлайн-планерки с заказчиком;
  • квалифицированных специалистов;
  • организованную команду (находятся в одном помещении, что упрощает решение рабочих вопросов);
  • полную прозрачность и регулярность отчетов о результатах.

Для доработок и развития мы предлагаем формат 100 часов в месяц. Что можно сделать за это время:

  • новые нетиповые страницы или раздел;
  • 2 отчета с индивидуальными настройками;
  • 3-5 веб-сервисов интеграции;
  • замудренный калькулятор и т.п.

Поддержка «чтобы все работало как часы» стоит 45 тысяч рублей в месяц и описана тут.

Хочешь получать лучшие статьи от INTERVOLGA раз в месяц?
Подпишись на рассылку — спамить не будем