Исповедь Битрикс хейтера

в 6:15, , рубрики: 1С-Битрикс, cms, hatebitrix, php, web-разработка, возьми себя в руки, нытики на хабре, Программирование, Разработка веб-сайтов, Чулан

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

Данная статья — попытка этот обзор написать. Хотя нет, это скорее пост ненависти и боли (может даже немного нытья). Это такой расширенный вариант поста про минусы от pistol. Я постараюсь описать большинство тех вещей, которые раздражают именно меня и моих коллег в Битриксе. Постараюсь собрать в одном посте все те минусы, которые доставляют ежедневно очень много боли. Под конец я постараюсь сделать выводы.

Кто я такой? Да в общем-то, обычный разработчик. Работаю с битриксом с ноября 2010 года (5.5 лет). Работаю только с битриксом, не сделал ни одного коммерческого проекта на других CMS, не использовал фреймворки в создании сайтов. По роду деятельности я занимаюсь в основном интернет-магазинами, их созданием, поддержкой и развитием.

TL;DR

Битрикс — УГ, не стоит лезть в этот омут без особой надобности.

Вместо вступления

Для начала я предлагаю вам провести мысленный эксперимент. Давайте попробуем взять двух backend-разработчиков примерно одного возраста и примерно с одинаковым стажем работы (допустим, 1 — 1.5 года), только чтобы один из них работал все это время с 1С-Битрикс, а другой — с Symfony(например). Можно легко сравнить, с каким набором технологий работал все это время один, а с каким — другой, и какой в итоге набор знаний они получили за это время.

В случае с Symfony разработчиком это будет: php5/7 + глубокое понимание ООП, общепринятые паттерны проектирования (MVC, DI, Factory, Repository как минимум), умение разрабатывать Unit тесты, использовать шаблонизаторы (минимум twig), ORM (с Doctrine), composer, git, стандарты PSR, опыт работы с консолью и написания консольных приложений, базовые навыки настройки веб-сервера.

В случае с 1С-Битрикс разработчиком это будет php5, html/css + javascript/jquery (из коробки шаблонизаторов нет, а битрикс сует логику в шаблоны, как ни крути, придется с этим возиться), возможно git (и это сильно зависит от компании, некоторые динозавры до сих пор пилят на продакшене через FTP), если повезет — немножно sql и… все?

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

Лишь однажды к нам в компанию пришел опытный человек не из мира 1С-Битрикс (в регионе) и он был на голову сильнее своих коллег с тем же стажем просто за счет того, что ранее ему поставили мозги на правильные рельсы.
Я и сам такой. Мне, к сожалению, с самого начала пустили пыль в глаза той же маркетинговой чепухой, да и попал я в не очень хорошее окружение. Я сам понимаю и чувствую, что мои коллеги с аналогичным стажем работы, но в том же Symfony, имеют больший кругозор, и это очень сильный побочный эффект от битрикса.
Это все наводит на мысли о том, что если ты хочешь развиваться в мире веб-разработки, то в качестве основы нужно выбирать уж точно не битрикс.

Сравнивая двух разработчиков, я хочу обратить внимание на те рамки, в которые загоняет система, и на ту свободу, которую она предоставляет. Что Битрикс, что Symfony — они оба дают почти безграничную гибкость, и в принципе на каждой из них можно создать продукт абсолютно любой сложности. Однако система должна помогать разработчику в решении проблем, вместо того, чтобы вставлять палки в колеса. И тут Битрикс очень сильно проигрывает.

Маркетинг

Сразу хочу сказать немного слов об этом, т.к. это основная составляющая успеха Битрикса.
Можно сказать, что духом маркетинга пропитан весь Битрикс, даже документация для разработчиков. Даже там они пишут о том, что их продукт «настолько крутой, что его ценят и уважают все наши партнеры»(пруф, блок «Структура»). В битриксе работают хорошие маркетологи, которые грамотно умеют преподнести свой продукт. Раз в полгода они устраивают конференции для партнеров, где рассказывают о том, что было сделано и о том, какие у них планы. Как показывает практика, никогда эти планы не сбываются в срок и очень часто релизы либо неполные, либо с кучей ошибок.
В качестве примера — нашумевший рефакторинг модуля sale, релиз которого откладывали больше года, и даже самую последнюю дату релиза (23 декабря 2015 года) провалили на 3 месяца, и выпустили новый магазин и БУС (Битрикс ред. «Управление Сайтом») 16 версии только в конце марта 2016го. Но в результате после обновления пользователи не только не получили новых фич. Пользователи получили в большинстве случаев неработоспособный магазин, и горку нового недокументированного кода в придачу.
Новым инструментам дают такие громкие названия, которые у всех на слуху: Композитный сайт — ускорение x100; Highload-блоки; Bitrix BigData. На самом деле за этими словами скрываются вполне обыденные вещи, которые не соответствуют своему имени.
И такой подход прослеживается везде, к сожалению. Снаружи продукт выглядит как конфетка, которую купил, поставил и пользуешься. Но если с битриксом сделать шаг в сторону от стандартной поставки — все, поддержание функциональности при обновлениях превращается в ад.
Впрочем, обо всем по порядку, тема маркетинга еще будет всплывать в этом посте, скорее всего, не раз.

Архитектура

На протяжении десятка лет Битрикс отчаянно загонял сам себя в тупик. Каждая новая фича в продукте выходила в соответствии с интересами бизнеса, без должной проработки с технической точки зрения. И, естественно, все это росло как снежный ком.
Если вдуматься, то в Битриксе нет архитектуры, как таковой. Нет даже общепринятых сформулированных правил, которые позволили бы следовать этой архитектуре. В курсе разработчиков, в разделе Архитектура продукта, сказано, что битрикс следует архитектуре MVC и приводит схему:

Битрикс MVC

Сразу хочу сказать, что это MVC очень сильно отличается от классического варианта. Тут очень сильная подмена понятий, никакого MVC тут на самом деле нет, просто есть некое абстрактное разделение на модули, компоненты, и шаблоны компонентов. А уже из этих кирпичиков строится весь сайт. Но каждый из этих кирпичиков может брать на себя разные задачи, и поэтому они тесно взаимосвязаны между собой.
Попробую рассмотреть каждый из этих аспектов архитектуры более подробно.

M — Model, или API

Мне сложно судить об API системы как о модели. Да, API предоставляет интерфейс доступа к данным и позволяет ими манипулировать. Но API битрикса позволяет работать не только с данными, но и с шаблонами, да и с пользовательскими запросами тоже. Ну да ладно… это лишь мое мнение.
На данный момент в Битриксе есть 2 варианта API. Условно можно разделить их на старое и новое. Новое API называется D7 (честно — не помню почему, но Rizhikov рассказывал об этом на одной из партнерских конференций).

Старое API — это собрание антипаттернов, ужасных примеров плохого кода. В Битриксе всегда считалось нормальным вызывать нестатические методы статически, и наоборот, требовать состояния тогда, когда это неуместно. Например, всем известный CIBlockElement::GetList — пожалуй, один из самых часто-используемых методов при разработке. Его реализация содержит более 500 строк кода, использует глобалки, строит ужасающие, колоссальные запросы, и содержит нереальный, просто нечитабельный недокументированный код.

Смотрим

function GetList($arOrder=array("SORT"=>"ASC"), $arFilter=array(), $arGroupBy=false, $arNavStartParams=false, $arSelectFields=array())
    {
        /*
        Filter combinations:
        CHECK_PERMISSIONS="N" - check permissions of the current user to the infoblock
            MIN_PERMISSION="R" - when permissions check, then minimal access level
        SHOW_HISTORY="N" - add history items to list
            SHOW_NEW="N" - if not add history items, then add new, but not published elements
        */
        global $DB, $USER;
        $MAX_LOCK = intval(COption::GetOptionString("workflow","MAX_LOCK_TIME","60"));
        $uid = is_object($USER)? intval($USER->GetID()): 0;

        $formatActiveDates = CPageOption::GetOptionString("iblock", "FORMAT_ACTIVE_DATES", "-") != "-";
        $shortFormatActiveDates = CPageOption::GetOptionString("iblock", "FORMAT_ACTIVE_DATES", "SHORT");

        $arIblockElementFields = array(
                "ID"=>"BE.ID",
                "TIMESTAMP_X"=>$DB->DateToCharFunction("BE.TIMESTAMP_X"),
                "TIMESTAMP_X_UNIX"=>'UNIX_TIMESTAMP(BE.TIMESTAMP_X)',
                "MODIFIED_BY"=>"BE.MODIFIED_BY",
                "DATE_CREATE"=>$DB->DateToCharFunction("BE.DATE_CREATE"),
                "DATE_CREATE_UNIX"=>'UNIX_TIMESTAMP(BE.DATE_CREATE)',
                "CREATED_BY"=>"BE.CREATED_BY",
                "IBLOCK_ID"=>"BE.IBLOCK_ID",
                "IBLOCK_SECTION_ID"=>"BE.IBLOCK_SECTION_ID",
                "ACTIVE"=>"BE.ACTIVE",
                "ACTIVE_FROM"=>(
                        $formatActiveDates
                        ?
                            $DB->DateToCharFunction("BE.ACTIVE_FROM", $shortFormatActiveDates)
                        :
                            "IF(EXTRACT(HOUR_SECOND FROM BE.ACTIVE_FROM)>0, ".$DB->DateToCharFunction("BE.ACTIVE_FROM", "FULL").", ".$DB->DateToCharFunction("BE.ACTIVE_FROM", "SHORT").")"
                        ),
                "ACTIVE_TO"=>(
                        $formatActiveDates
                        ?
                            $DB->DateToCharFunction("BE.ACTIVE_TO", $shortFormatActiveDates)
                        :
                            "IF(EXTRACT(HOUR_SECOND FROM BE.ACTIVE_TO)>0, ".$DB->DateToCharFunction("BE.ACTIVE_TO", "FULL").", ".$DB->DateToCharFunction("BE.ACTIVE_TO", "SHORT").")"
                        ),
                "DATE_ACTIVE_FROM"=>(
                        $formatActiveDates
                        ?
                            $DB->DateToCharFunction("BE.ACTIVE_FROM", $shortFormatActiveDates)
                        :
                            "IF(EXTRACT(HOUR_SECOND FROM BE.ACTIVE_FROM)>0, ".$DB->DateToCharFunction("BE.ACTIVE_FROM", "FULL").", ".$DB->DateToCharFunction("BE.ACTIVE_FROM", "SHORT").")"
                        ),
                "DATE_ACTIVE_TO"=>(
                        $formatActiveDates
                        ?
                            $DB->DateToCharFunction("BE.ACTIVE_TO", $shortFormatActiveDates)
                        :
                            "IF(EXTRACT(HOUR_SECOND FROM BE.ACTIVE_TO)>0, ".$DB->DateToCharFunction("BE.ACTIVE_TO", "FULL").", ".$DB->DateToCharFunction("BE.ACTIVE_TO", "SHORT").")"
                        ),
                "SORT"=>"BE.SORT",
                "NAME"=>"BE.NAME",
                "PREVIEW_PICTURE"=>"BE.PREVIEW_PICTURE",
                "PREVIEW_TEXT"=>"BE.PREVIEW_TEXT",
                "PREVIEW_TEXT_TYPE"=>"BE.PREVIEW_TEXT_TYPE",
                "DETAIL_PICTURE"=>"BE.DETAIL_PICTURE",
                "DETAIL_TEXT"=>"BE.DETAIL_TEXT",
                "DETAIL_TEXT_TYPE"=>"BE.DETAIL_TEXT_TYPE",
                "SEARCHABLE_CONTENT"=>"BE.SEARCHABLE_CONTENT",
                "WF_STATUS_ID"=>"BE.WF_STATUS_ID",
                "WF_PARENT_ELEMENT_ID"=>"BE.WF_PARENT_ELEMENT_ID",
                "WF_LAST_HISTORY_ID"=>"BE.WF_LAST_HISTORY_ID",
                "WF_NEW"=>"BE.WF_NEW",
                "LOCK_STATUS"=>"if (BE.WF_DATE_LOCK is null, 'green', if(DATE_ADD(BE.WF_DATE_LOCK, interval ".$MAX_LOCK." MINUTE)<now(), 'green', if(BE.WF_LOCKED_BY=".$uid.", 'yellow', 'red')))",
                "WF_LOCKED_BY"=>"BE.WF_LOCKED_BY",
                "WF_DATE_LOCK"=>$DB->DateToCharFunction("BE.WF_DATE_LOCK"),
                "WF_COMMENTS"=>"BE.WF_COMMENTS",
                "IN_SECTIONS"=>"BE.IN_SECTIONS",
                "SHOW_COUNTER"=>"BE.SHOW_COUNTER",
                "SHOW_COUNTER_START"=>$DB->DateToCharFunction("BE.SHOW_COUNTER_START"),
                "CODE"=>"BE.CODE",
                "TAGS"=>"BE.TAGS",
                "XML_ID"=>"BE.XML_ID",
                "EXTERNAL_ID"=>"BE.XML_ID",
                "TMP_ID"=>"BE.TMP_ID",
                "USER_NAME"=>"concat('(',U.LOGIN,') ',ifnull(U.NAME,''),' ',ifnull(U.LAST_NAME,''))",
                "LOCKED_USER_NAME"=>"concat('(',UL.LOGIN,') ',ifnull(UL.NAME,''),' ',ifnull(UL.LAST_NAME,''))",
                "CREATED_USER_NAME"=>"concat('(',UC.LOGIN,') ',ifnull(UC.NAME,''),' ',ifnull(UC.LAST_NAME,''))",
                "LANG_DIR"=>"L.DIR",
                "LID"=>"B.LID",
                "IBLOCK_TYPE_ID"=>"B.IBLOCK_TYPE_ID",
                "IBLOCK_CODE"=>"B.CODE",
                "IBLOCK_NAME"=>"B.NAME",
                "IBLOCK_EXTERNAL_ID"=>"B.XML_ID",
                "DETAIL_PAGE_URL"=>"B.DETAIL_PAGE_URL",
                "LIST_PAGE_URL"=>"B.LIST_PAGE_URL",
                "CANONICAL_PAGE_URL"=>"B.CANONICAL_PAGE_URL",
                "CREATED_DATE"=>$DB->DateFormatToDB("YYYY.MM.DD", "BE.DATE_CREATE"),
                "BP_PUBLISHED"=>"if(BE.WF_STATUS_ID = 1, 'Y', 'N')",
            );
        unset($shortFormatActiveDates);
        unset($formatActiveDates);

        $bDistinct = false;

        CIBlockElement::PrepareGetList(
                $arIblockElementFields,
                $arJoinProps,
                $bOnlyCount,
                $bDistinct,

                $arSelectFields,
                $sSelect,
                $arAddSelectFields,

                $arFilter,
                $sWhere,
                $sSectionWhere,
                $arAddWhereFields,

                $arGroupBy,
                $sGroupBy,

                $arOrder,
                $arSqlOrder,
                $arAddOrderByFields,

                $arIBlockFilter,
                $arIBlockMultProps,
                $arIBlockConvProps,
                $arIBlockAllProps,
                $arIBlockNumProps,
                $arIBlockLongProps
            );

        $arFilterIBlocks = isset($arFilter["IBLOCK_ID"])? array($arFilter["IBLOCK_ID"]): array();
        //******************FROM PART********************************************
        $sFrom = "";
        foreach($arJoinProps["FPS"] as $iblock_id => $iPropCnt)
        {
            $sFrom .= "tttINNER JOIN b_iblock_element_prop_s".$iblock_id." FPS".$iPropCnt." ON FPS".$iPropCnt.".IBLOCK_ELEMENT_ID = BE.IDn";
            $arFilterIBlocks[$iblock_id] = $iblock_id;
        }

        foreach($arJoinProps["FP"] as $propID => $db_prop)
        {
            $i = $db_prop["CNT"];

            if($db_prop["bFullJoin"])
                $sFrom .= "tttINNER JOIN b_iblock_property FP".$i." ON FP".$i.".IBLOCK_ID = B.ID AND ".
                    (
                        IntVal($propID)>0?
                        " FP".$i.".ID=".IntVal($propID)."n":
                        " FP".$i.".CODE='".$DB->ForSQL($propID, 200)."'n"
                    );
            else
                $sFrom .= "tttLEFT JOIN b_iblock_property FP".$i." ON FP".$i.".IBLOCK_ID = B.ID AND ".
                    (
                        IntVal($propID)>0?
                        " FP".$i.".ID=".IntVal($propID)."n":
                        " FP".$i.".CODE='".$DB->ForSQL($propID, 200)."'n"
                    );

            if($db_prop["IBLOCK_ID"])
                $arFilterIBlocks[$db_prop["IBLOCK_ID"]] = $db_prop["IBLOCK_ID"];
        }

        foreach($arJoinProps["FPV"] as $propID => $db_prop)
        {
            $i = $db_prop["CNT"];

            if($db_prop["MULTIPLE"]=="Y")
                $bDistinct = true;

            if($db_prop["VERSION"]==2)
                $strTable = "b_iblock_element_prop_m".$db_prop["IBLOCK_ID"];
            else
                $strTable = "b_iblock_element_property";

            if($db_prop["bFullJoin"])
                $sFrom .= "tttINNER JOIN ".$strTable." FPV".$i." ON FPV".$i.".IBLOCK_PROPERTY_ID = FP".$db_prop["JOIN"].".ID AND FPV".$i.".IBLOCK_ELEMENT_ID = BE.IDn";
            else
                $sFrom .= "tttLEFT JOIN ".$strTable." FPV".$i." ON FPV".$i.".IBLOCK_PROPERTY_ID = FP".$db_prop["JOIN"].".ID AND FPV".$i.".IBLOCK_ELEMENT_ID = BE.IDn";

            if($db_prop["IBLOCK_ID"])
                $arFilterIBlocks[$db_prop["IBLOCK_ID"]] = $db_prop["IBLOCK_ID"];
        }

        foreach($arJoinProps["FPEN"] as $propID => $db_prop)
        {
            $i = $db_prop["CNT"];

            if($db_prop["VERSION"] == 2 && $db_prop["MULTIPLE"] == "N")
            {
                if($db_prop["bFullJoin"])
                    $sFrom .= "tttINNER JOIN b_iblock_property_enum FPEN".$i." ON FPEN".$i.".PROPERTY_ID = ".$db_prop["ORIG_ID"]." AND FPS".$db_prop["JOIN"].".PROPERTY_".$db_prop["ORIG_ID"]." = FPEN".$i.".IDn";
                else
                    $sFrom .= "tttLEFT JOIN b_iblock_property_enum FPEN".$i." ON FPEN".$i.".PROPERTY_ID = ".$db_prop["ORIG_ID"]." AND FPS".$db_prop["JOIN"].".PROPERTY_".$db_prop["ORIG_ID"]." = FPEN".$i.".IDn";
            }
            else
            {
                if($db_prop["bFullJoin"])
                    $sFrom .= "tttINNER JOIN b_iblock_property_enum FPEN".$i." ON FPEN".$i.".PROPERTY_ID = FPV".$db_prop["JOIN"].".IBLOCK_PROPERTY_ID AND FPV".$db_prop["JOIN"].".VALUE_ENUM = FPEN".$i.".IDn";
                else
                    $sFrom .= "tttLEFT JOIN b_iblock_property_enum FPEN".$i." ON FPEN".$i.".PROPERTY_ID = FPV".$db_prop["JOIN"].".IBLOCK_PROPERTY_ID AND FPV".$db_prop["JOIN"].".VALUE_ENUM = FPEN".$i.".IDn";
            }

            if($db_prop["IBLOCK_ID"])
                $arFilterIBlocks[$db_prop["IBLOCK_ID"]] = $db_prop["IBLOCK_ID"];
        }

        foreach($arJoinProps["BE"] as $propID => $db_prop)
        {
            $i = $db_prop["CNT"];

            $sFrom .= "tttLEFT JOIN b_iblock_element BE".$i." ON BE".$i.".ID = ".
                (
                    $db_prop["VERSION"]==2 && $db_prop["MULTIPLE"]=="N"?
                    "FPS".$db_prop["JOIN"].".PROPERTY_".$db_prop["ORIG_ID"]
                    :"FPV".$db_prop["JOIN"].".VALUE_NUM"
                ).
                (
                    $arFilter["SHOW_HISTORY"] != "Y"?
                    " AND ((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL)".($arFilter["SHOW_NEW"]=="Y"? " OR BE.WF_NEW='Y'": "").")":
                    ""
                )."n";

            if($db_prop["bJoinIBlock"])
                $sFrom .= "tttLEFT JOIN b_iblock B".$i." ON B".$i.".ID = BE".$i.".IBLOCK_IDn";

            if($db_prop["bJoinSection"])
                $sFrom .= "tttLEFT JOIN b_iblock_section BS".$i." ON BS".$i.".ID = BE".$i.".IBLOCK_SECTION_IDn";

            if($db_prop["IBLOCK_ID"])
                $arFilterIBlocks[$db_prop["IBLOCK_ID"]] = $db_prop["IBLOCK_ID"];
        }

        foreach($arJoinProps["BE_FPS"] as $iblock_id => $db_prop)
        {
            $sFrom .= "tttLEFT JOIN b_iblock_element_prop_s".$iblock_id." JFPS".$db_prop["CNT"]." ON JFPS".$db_prop["CNT"].".IBLOCK_ELEMENT_ID = BE".$db_prop["JOIN"].".IDn";

            if($db_prop["IBLOCK_ID"])
                $arFilterIBlocks[$db_prop["IBLOCK_ID"]] = $db_prop["IBLOCK_ID"];
        }

        foreach($arJoinProps["BE_FP"] as $propID => $db_prop)
        {
            $i = $db_prop["CNT"];
            list($propID, $link) = explode("~", $propID, 2);

            if($db_prop["bFullJoin"])
                $sFrom .= "tttINNER JOIN b_iblock_property JFP".$i." ON JFP".$i.".IBLOCK_ID = BE".$db_prop["JOIN"].".IBLOCK_ID AND ".
                    (
                        IntVal($propID)>0?
                        " JFP".$i.".ID=".IntVal($propID)."n":
                        " JFP".$i.".CODE='".$DB->ForSQL($propID, 200)."'n"
                    );
            else
                $sFrom .= "tttLEFT JOIN b_iblock_property JFP".$i." ON JFP".$i.".IBLOCK_ID = BE".$db_prop["JOIN"].".IBLOCK_ID AND ".
                    (
                        IntVal($propID)>0?
                        " JFP".$i.".ID=".IntVal($propID)."n":
                        " JFP".$i.".CODE='".$DB->ForSQL($propID, 200)."'n"
                    );

            if($db_prop["IBLOCK_ID"])
                $arFilterIBlocks[$db_prop["IBLOCK_ID"]] = $db_prop["IBLOCK_ID"];
        }

        foreach($arJoinProps["BE_FPV"] as $propID => $db_prop)
        {
            $i = $db_prop["CNT"];
            list($propID, $link) = explode("~", $propID, 2);

            if($db_prop["MULTIPLE"]=="Y")
                $bDistinct = true;

            if($db_prop["VERSION"]==2)
                $strTable = "b_iblock_element_prop_m".$db_prop["IBLOCK_ID"];
            else
                $strTable = "b_iblock_element_property";

            if($db_prop["bFullJoin"])
                $sFrom .= "tttINNER JOIN ".$strTable." JFPV".$i." ON JFPV".$i.".IBLOCK_PROPERTY_ID = JFP".$db_prop["JOIN"].".ID AND JFPV".$i.".IBLOCK_ELEMENT_ID = BE".$db_prop["BE_JOIN"].".IDn";
            else
                $sFrom .= "tttLEFT JOIN ".$strTable." JFPV".$i." ON JFPV".$i.".IBLOCK_PROPERTY_ID = JFP".$db_prop["JOIN"].".ID AND JFPV".$i.".IBLOCK_ELEMENT_ID = BE".$db_prop["BE_JOIN"].".IDn";

            if($db_prop["IBLOCK_ID"])
                $arFilterIBlocks[$db_prop["IBLOCK_ID"]] = $db_prop["IBLOCK_ID"];
        }

        foreach($arJoinProps["BE_FPEN"] as $propID => $db_prop)
        {
            $i = $db_prop["CNT"];
            list($propID, $link) = explode("~", $propID, 2);

            if($db_prop["VERSION"] == 2 && $db_prop["MULTIPLE"] == "N")
            {
                if($db_prop["bFullJoin"])
                    $sFrom .= "tttINNER JOIN b_iblock_property_enum JFPEN".$i." ON JFPEN".$i.".PROPERTY_ID = ".$db_prop["ORIG_ID"]." AND JFPS".$db_prop["JOIN"].".PROPERTY_".$db_prop["ORIG_ID"]." = JFPEN".$i.".IDn";
                else
                    $sFrom .= "tttLEFT JOIN b_iblock_property_enum JFPEN".$i." ON JFPEN".$i.".PROPERTY_ID = ".$db_prop["ORIG_ID"]." AND JFPS".$db_prop["JOIN"].".PROPERTY_".$db_prop["ORIG_ID"]." = JFPEN".$i.".IDn";
            }
            else
            {
                if($db_prop["bFullJoin"])
                    $sFrom .= "tttINNER JOIN b_iblock_property_enum JFPEN".$i." ON JFPEN".$i.".PROPERTY_ID = JFPV".$db_prop["JOIN"].".IBLOCK_PROPERTY_ID AND JFPV".$db_prop["JOIN"].".VALUE_ENUM = JFPEN".$i.".IDn";
                else
                    $sFrom .= "tttLEFT JOIN b_iblock_property_enum JFPEN".$i." ON JFPEN".$i.".PROPERTY_ID = JFPV".$db_prop["JOIN"].".IBLOCK_PROPERTY_ID AND JFPV".$db_prop["JOIN"].".VALUE_ENUM = JFPEN".$i.".IDn";
            }

            if($db_prop["IBLOCK_ID"])
                $arFilterIBlocks[$db_prop["IBLOCK_ID"]] = $db_prop["IBLOCK_ID"];
        }

        if(strlen($arJoinProps["BES"]))
        {
            $sFrom .= "ttt".$arJoinProps["BES"]."n";
        }

        if(strlen($arJoinProps["FC"]))
        {
            $sFrom .= "ttt".$arJoinProps["FC"]."n";
            $bDistinct = $bDistinct || (isset($arJoinProps["FC_DISTINCT"]) && $arJoinProps["FC_DISTINCT"] == "Y");
        }

        if($arJoinProps["RV"])
            $sFrom .= "tttLEFT JOIN b_rating_voting RV ON RV.ENTITY_TYPE_ID = 'IBLOCK_ELEMENT' AND RV.ENTITY_ID = BE.IDn";
        if($arJoinProps["RVU"])
            $sFrom .= "tttLEFT JOIN b_rating_vote RVU ON RVU.ENTITY_TYPE_ID = 'IBLOCK_ELEMENT' AND RVU.ENTITY_ID = BE.ID AND RVU.USER_ID = ".$uid."n";
        if($arJoinProps["RVV"])
            $sFrom .= "ttt".($arJoinProps["RVV"]["bFullJoin"]? "INNER": "LEFT")." JOIN b_rating_vote RVV ON RVV.ENTITY_TYPE_ID = 'IBLOCK_ELEMENT' AND RVV.ENTITY_ID = BE.IDn";

        //******************END OF FROM PART********************************************

        $bCatalogSort = false;
        if(count($arAddSelectFields)>0 || count($arAddWhereFields)>0 || count($arAddOrderByFields)>0)
        {
            if(CModule::IncludeModule("catalog"))
            {
                $res_catalog = CCatalogProduct::GetQueryBuildArrays($arAddOrderByFields, $arAddWhereFields, $arAddSelectFields);
                if(
                    $sGroupBy==""
                    && !$bOnlyCount
                    && !(is_object($this) && isset($this->strField))
                )
                    $sSelect .= $res_catalog["SELECT"]." ";
                $sFrom .= str_replace("LEFT JOIN", "ntttLEFT JOIN", $res_catalog["FROM"])."n";
                //$sWhere .= $res_catalog["WHERE"]." "; moved to MkFilter
                if(is_array($res_catalog["ORDER"]) && count($res_catalog["ORDER"]))
                {
                    $bCatalogSort = true;
                    foreach($res_catalog["ORDER"] as $i=>$val)
                        $arSqlOrder[$i] = $val;
                }
            }
        }

        $i = array_search("CREATED_BY_FORMATTED", $arSelectFields);
        if ($i !== false)
        {
            if (
                $sSelect
                && $sGroupBy==""
                && !$bOnlyCount
                && !(is_object($this) && isset($this->strField))
            )
            {
                $sSelect .= ",UC.NAME UC_NAME, UC.LAST_NAME UC_LAST_NAME, UC.SECOND_NAME UC_SECOND_NAME, UC.EMAIL UC_EMAIL, UC.ID UC_ID, UC.LOGIN UC_LOGIN";
            }
            else
            {
                unset($arSelectFields[$i]);
            }
        }

        $sOrderBy = "";
        foreach($arSqlOrder as $i=>$val)
        {
            if(strlen($val))
            {
                if($sOrderBy=="")
                    $sOrderBy = " ORDER BY ";
                else
                    $sOrderBy .= ",";

                $sOrderBy .= $val." ";
            }
        }

        $sSelect = trim($sSelect, ", tnr");
        if(strlen($sSelect) <= 0)
            $sSelect = "0 as NOP ";

        $bDistinct = $bDistinct || (isset($arFilter["INCLUDE_SUBSECTIONS"]) && $arFilter["INCLUDE_SUBSECTIONS"] == "Y");

        if($bDistinct)
            $sSelect = str_replace("%%_DISTINCT_%%", "DISTINCT", $sSelect);
        else
            $sSelect = str_replace("%%_DISTINCT_%%", "", $sSelect);

        $sFrom = "
            b_iblock B
            INNER JOIN b_lang L ON B.LID=L.LID
            INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
            ".ltrim($sFrom, "tn")
            .(in_array("USER_NAME", $arSelectFields)? "tttLEFT JOIN b_user U ON U.ID=BE.MODIFIED_BYn": "")
            .(in_array("LOCKED_USER_NAME", $arSelectFields)? "tttLEFT JOIN b_user UL ON UL.ID=BE.WF_LOCKED_BYn": "")
            .(in_array("CREATED_USER_NAME", $arSelectFields) || in_array("CREATED_BY_FORMATTED", $arSelectFields)? "tttLEFT JOIN b_user UC ON UC.ID=BE.CREATED_BYn": "")."
        ";

        $strSql = "
            FROM ".$sFrom."
            WHERE 1=1 "
            .$sWhere."
            ".$sGroupBy."
        ";

        if(isset($this) && is_object($this) && isset($this->strField))
        {
            $this->sFrom = $sFrom;
            $this->sWhere = $sWhere;
            return "SELECT ".$sSelect.$strSql;
        }

        if($bOnlyCount)
        {
            $res = $DB->Query("SELECT ".$sSelect.$strSql, false, "FILE: ".__FILE__."<br> LINE: ".__LINE__);
            $res = $res->Fetch();
            return $res["CNT"];
        }

        if(is_array($arNavStartParams))
        {
            $nTopCount = intval($arNavStartParams["nTopCount"]);
            $nElementID = intval($arNavStartParams["nElementID"]);

            if($nTopCount > 0)
            {
                $strSql = "SELECT ".$sSelect.$strSql.$sOrderBy." LIMIT ".$nTopCount;
                $res = $DB->Query($strSql);
            }
            elseif(
                $nElementID > 0
                && $sGroupBy == ""
                && $sOrderBy != ""
                && strpos($sSelect, "BE.ID") !== false
                && !$bCatalogSort
            )
            {
                $nPageSize = intval($arNavStartParams["nPageSize"]);

                if($nPageSize > 0)
                {
                    $DB->Query("SET @rank_e=0");

                    $DB->Query("SET @rank_r=0");
                    $DB->Query("
                        SELECT
                            ".$sSelect."
                            ,@rank_r:=@rank_r+1 AS rank1
                            ,if (BE.ID = ".$nElementID.", @rank_e:=@rank_r, null) rank2
                        ".$strSql.$sOrderBy."
                    ");

                    $DB->Query("SET @rank_r=0");
                    $res = $DB->Query("
                        SELECT *
                        FROM (
                            SELECT
                                ".$sSelect."
                                ,@rank_r:=@rank_r+1 AS RANK
                            ".$strSql.$sOrderBy."
                            LIMIT 18446744073709551615
                        ) el0
                        WHERE el0.RANK between @rank_e-$nPageSize and @rank_e+$nPageSize
                    ");
                }
                else
                {
                    $DB->Query("SET @rank=0");
                    $res = $DB->Query("
                        SELECT *
                        FROM (
                            SELECT
                                ".$sSelect."
                                ,@rank:=@rank+1 AS RANK
                            ".$strSql.$sOrderBy."
                            LIMIT 18446744073709551615
                        ) el0
                        WHERE el0.ID = ".$nElementID."
                    ");
                }
            }
            else
            {
                if($sGroupBy == "")
                {
                    $res_cnt = $DB->Query("SELECT COUNT(".($bDistinct? "DISTINCT BE.ID": "'x'").") as C ".$strSql);
                    $res_cnt = $res_cnt->Fetch();
                    $cnt = $res_cnt["C"];
                }
                else
                {
                    $res_cnt = $DB->Query("SELECT 'x' ".$strSql);
                    $cnt = $res_cnt->SelectedRowsCount();
                }

                $strSql = "SELECT ".$sSelect.$strSql.$sOrderBy;
                $res = new CDBResult();
                $res->NavQuery($strSql, $cnt, $arNavStartParams);
            }
        }
        else//if(is_array($arNavStartParams))
        {
            $strSql = "SELECT ".$sSelect.$strSql.$sOrderBy;
            $res = $DB->Query($strSql, false, "FILE: ".__FILE__."<br> LINE: ".__LINE__);
        }

        $res = new CIBlockResult($res);
        $res->SetIBlockTag($arFilterIBlocks);
        $res->arIBlockMultProps = $arIBlockMultProps;
        $res->arIBlockConvProps = $arIBlockConvProps;
        $res->arIBlockAllProps  = $arIBlockAllProps;
        $res->arIBlockNumProps = $arIBlockNumProps;
        $res->arIBlockLongProps = $arIBlockLongProps;

        return $res;
    }

Как можно догадаться, этот метод получает из БД список элементов инфоблока, и для получения списка не требуется создания экземпляра класса CIBlockElement. Однако, чтобы добавить элемент инфоблока, обязательно нужно состояние, и только для того, чтобы записать инфо о последней произошедшей ошибке в публичное свойство класса.

В старом API очень активно используются такие глобальные переменные, как $APPLICATION, $USER, $DB. Они являются экземплярами определенных классов, и в документации раньше гордо звались синглтонами, правда сейчас я не нашел уже этих слов.
Для того, чтобы сгенерировать ошибку, например, в обработчиках событий, нужно воспользоваться методом $APPLICATION->ThrowException(), который на самом деле исключения не бросает.

    public function ThrowException($msg, $id = false)
    {
        $this->ResetException();
        if(is_object($msg) && (is_subclass_of($msg, 'CApplicationException') || (strtolower(get_class($msg))=='capplicationexception')))
            $this->LAST_ERROR = $msg;
        else
            $this->LAST_ERROR = new CApplicationException($msg, $id);
    }

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

Новое API уже несколько отличается от старого. Во-первых, весь код из нового ядра разложен по неймспейсам, где прослеживается четкая зависимость от модуля. Например, аналог CIBlockElement::GetList из нового ядра — BitrixIblockElementTable::getList, где корневое пространство имен — это имя вендора, а следующее — имя модуля. Для того, чтобы это работало, в битриксе написали свой автозагрузчик BitrixMainLoader::autoLoad, который совсем не совместим с PSR-0/4.

Собственно, код автозагрузчика в виде одной функции

        public static function autoLoad($className)
	{
		$file = ltrim($className, "\");    // fix web env
		$file = strtr($file, static::ALPHA_UPPER, static::ALPHA_LOWER);

		static $documentRoot = null;
		if ($documentRoot === null)
			$documentRoot = static::getDocumentRoot();

		if (isset(self::$arAutoLoadClasses[$file]))
		{
			$pathInfo = self::$arAutoLoadClasses[$file];
			if ($pathInfo["module"] != "")
			{
				$m = $pathInfo["module"];
				$h = isset(self::$arLoadedModulesHolders[$m]) ? self::$arLoadedModulesHolders[$m] : 'bitrix';
				include_once($documentRoot."/".$h."/modules/".$m."/" .$pathInfo["file"]);
			}
			else
			{
				require_once($documentRoot.$pathInfo["file"]);
			}
			return;
		}

		if (preg_match("#[^\\/a-zA-Z0-9_]#", $file))
			return;

		if (substr($file, -5) == "table")
			$file = substr($file, 0, -5);

		$file = str_replace('\', '/', $file);
		$arFile = explode("/", $file);

		if ($arFile[0] === "bitrix")
		{
			array_shift($arFile);

			if (empty($arFile))
				return;

			$module = array_shift($arFile);
			if ($module == null || empty($arFile))
				return;
		}
		else
		{
			$module1 = array_shift($arFile);
			$module2 = array_shift($arFile);
			if ($module1 == null || $module2 == null || empty($arFile))
				return;

			$module = $module1.".".$module2;
		}

		if (!isset(self::$arLoadedModulesHolders[$module]))
			return;

		$filePath = $documentRoot."/".self::$arLoadedModulesHolders[$module]."/modules/".$module."/lib/".implode("/", $arFile).".php";

		if (file_exists($filePath))
			require_once($filePath);
	}

В новом API прослеживается большая любовь к Singleton:

  • BitrixMainApplication::getInstance — инстанс приложения
  • BitrixMainConfigConfiguration::getInstance — инстанс класса для управления конфигами
  • BitrixMainPageAsset::getInstance — инстанс Asset-менеджера
  • BitrixMainEventManager::getInstance — менеджер событий

Возможно, это все в будущем обрастет своим ServiceLayer'ом (есть некий BitrixMainServiceManager в новом ядре, который пока не используется и не документирован). Но надежды пока мало.

ORM — еще одно из нововведений D7, и это уже что-то, что может претендовать на звание настоящей модели! Отличить класс сущности ORM от любого другого класса можно по его имени. Класс сущности всегда должен заканчиваться на Table (ElementTable, SectionTable, OrderTable и т.д.). Причем, парадокс, имя файла с классом ORM сущности не должно заканчиваться на Table. К примеру, для ElementTable мы должны создать файл element.php. Ниже на скрине представлено содержимое директории lib (только в этой директории работает автозагрузка D7) модуля iblock. Попробуйте на глазок определить, что есть сущности ORM, а что — обычные классы с бизнес-логикой.

Структура модуля iblock

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

Пример типичного класса сущности ORM для элемента инфоблока

<?
namespace BitrixIblock;

use BitrixMain;
use BitrixMainLocalizationLoc;

Loc::loadMessages(__FILE__);

/**
 * Class ElementTable
 *
 * Fields:
 * <ul>
 * <li> ID int mandatory
 * <li> TIMESTAMP_X datetime optional
 * <li> MODIFIED_BY int optional
 * <li> DATE_CREATE datetime optional
 * <li> CREATED_BY int optional
 * <li> IBLOCK_ID int mandatory
 * <li> IBLOCK_SECTION_ID int optional
 * <li> ACTIVE bool optional default 'Y'
 * <li> ACTIVE_FROM datetime optional
 * <li> ACTIVE_TO datetime optional
 * <li> SORT int optional default 500
 * <li> NAME string(255) mandatory
 * <li> PREVIEW_PICTURE int optional
 * <li> PREVIEW_TEXT string optional
 * <li> PREVIEW_TEXT_TYPE enum ('text', 'html') optional default 'text'
 * <li> DETAIL_PICTURE int optional
 * <li> DETAIL_TEXT string optional
 * <li> DETAIL_TEXT_TYPE enum ('text', 'html') optional default 'text'
 * <li> SEARCHABLE_CONTENT string optional
 * <li> WF_STATUS_ID int optional default 1
 * <li> WF_PARENT_ELEMENT_ID int optional
 * <li> WF_NEW enum ('N', 'Y') optional
 * <li> WF_LOCKED_BY int optional
 * <li> WF_DATE_LOCK datetime optional
 * <li> WF_COMMENTS string optional
 * <li> IN_SECTIONS bool optional default 'N'
 * <li> XML_ID string(255) optional
 * <li> CODE string(255) optional
 * <li> TAGS string(255) optional
 * <li> TMP_ID string(40) optional
 * <li> WF_LAST_HISTORY_ID int optional
 * <li> SHOW_COUNTER int optional
 * <li> SHOW_COUNTER_START datetime optional
 * <li> PREVIEW_PICTURE_FILE reference to {@link BitrixFileFileTable}
 * <li> DETAIL_PICTURE_FILE reference to {@link BitrixFileFileTable}
 * <li> IBLOCK reference to {@link BitrixIblockIblockTable}
 * <li> WF_PARENT_ELEMENT reference to {@link BitrixIblockIblockElementTable}
 * <li> IBLOCK_SECTION reference to {@link BitrixIblockIblockSectionTable}
 * <li> MODIFIED_BY_USER reference to {@link BitrixUserUserTable}
 * <li> CREATED_BY_USER reference to {@link BitrixUserUserTable}
 * <li> WF_LOCKED_BY_USER reference to {@link BitrixUserUserTable}
 * </ul>
 *
 * @package BitrixIblock
 **/

class ElementTable extends MainEntityDataManager
{
	const TYPE_TEXT = 'text';
	const TYPE_HTML = 'html';

	/**
	 * Returns DB table name for entity.
	 *
	 * @return string
	 */
	public static function getTableName()
	{
		return 'b_iblock_element';
	}

	/**
	 * Returns entity map definition.
	 *
	 * @return array
	 */
	public static function getMap()
	{
		return array(
			'ID' => new MainEntityIntegerField('ID', array(
				'primary' => true,
				'autocomplete' => true,
				'title' => Loc::getMessage('ELEMENT_ENTITY_ID_FIELD'),
			)),
			'TIMESTAMP_X' => new MainEntityDatetimeField('TIMESTAMP_X', array(
				'default_value' => new MainTypeDateTime(),
				'title' => Loc::getMessage('ELEMENT_ENTITY_TIMESTAMP_X_FIELD'),
			)),
			'MODIFIED_BY' => new MainEntityIntegerField('MODIFIED_BY', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_MODIFIED_BY_FIELD'),
			)),
			'DATE_CREATE' => new MainEntityDatetimeField('DATE_CREATE', array(
				'default_value' => new MainTypeDateTime(),
				'title' => Loc::getMessage('ELEMENT_ENTITY_DATE_CREATE_FIELD'),
			)),
			'CREATED_BY' => new MainEntityIntegerField('CREATED_BY', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_CREATED_BY_FIELD'),
			)),
			'IBLOCK_ID' => new MainEntityIntegerField('IBLOCK_ID', array(
				'required' => true,
				'title' => Loc::getMessage('ELEMENT_ENTITY_IBLOCK_ID_FIELD'),
			)),
			'IBLOCK_SECTION_ID' => new MainEntityIntegerField('IBLOCK_SECTION_ID', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_IBLOCK_SECTION_ID_FIELD'),
			)),
			'ACTIVE' => new MainEntityBooleanField('ACTIVE', array(
				'values' => array('N', 'Y'),
				'default_value' => 'Y',
				'title' => Loc::getMessage('ELEMENT_ENTITY_ACTIVE_FIELD'),
			)),
			'ACTIVE_FROM' => new MainEntityDatetimeField('ACTIVE_FROM', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_ACTIVE_FROM_FIELD'),
			)),
			'ACTIVE_TO' => new MainEntityDatetimeField('ACTIVE_TO', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_ACTIVE_TO_FIELD'),
			)),
			'SORT' => new MainEntityIntegerField('SORT', array(
				'default_value' => 500,
				'title' => Loc::getMessage('ELEMENT_ENTITY_SORT_FIELD'),
			)),
			'NAME' => new MainEntityStringField('NAME', array(
				'required' => true,
				'validation' => array(__CLASS__, 'validateName'),
				'title' => Loc::getMessage('ELEMENT_ENTITY_NAME_FIELD'),
			)),
			'PREVIEW_PICTURE' => new MainEntityIntegerField('PREVIEW_PICTURE', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_PREVIEW_PICTURE_FIELD'),
			)),
			'PREVIEW_TEXT' => new MainEntityTextField('PREVIEW_TEXT', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_PREVIEW_TEXT_FIELD'),
			)),
			'PREVIEW_TEXT_TYPE' => new MainEntityEnumField('PREVIEW_TEXT_TYPE', array(
				'values' => array(self::TYPE_TEXT, self::TYPE_HTML),
				'default_value' => self::TYPE_TEXT,
				'title' => Loc::getMessage('ELEMENT_ENTITY_PREVIEW_TEXT_TYPE_FIELD'),
			)),
			'DETAIL_PICTURE' => new MainEntityIntegerField('DETAIL_PICTURE', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_DETAIL_PICTURE_FIELD'),
			)),
			'DETAIL_TEXT' => new MainEntityTextField('DETAIL_TEXT', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_DETAIL_TEXT_FIELD'),
			)),
			'DETAIL_TEXT_TYPE' => new MainEntityEnumField('DETAIL_TEXT_TYPE', array(
				'values' => array(self::TYPE_TEXT, self::TYPE_HTML),
				'default_value' => self::TYPE_TEXT,
				'title' => Loc::getMessage('ELEMENT_ENTITY_DETAIL_TEXT_TYPE_FIELD'),
			)),
			'SEARCHABLE_CONTENT' => new MainEntityTextField('SEARCHABLE_CONTENT', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_SEARCHABLE_CONTENT_FIELD'),
			)),
			'WF_STATUS_ID' => new MainEntityIntegerField('WF_STATUS_ID', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_WF_STATUS_ID_FIELD'),
			)),
			'WF_PARENT_ELEMENT_ID' => new MainEntityIntegerField('WF_PARENT_ELEMENT_ID', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_WF_PARENT_ELEMENT_ID_FIELD'),
			)),
			'WF_NEW' => new MainEntityEnumField('WF_NEW', array(
				'values' => array('N', 'Y'),
				'title' => Loc::getMessage('ELEMENT_ENTITY_WF_NEW_FIELD'),
			)),
			'WF_LOCKED_BY' => new MainEntityIntegerField('WF_LOCKED_BY', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_WF_LOCKED_BY_FIELD'),
			)),
			'WF_DATE_LOCK' => new MainEntityDatetimeField('WF_DATE_LOCK', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_WF_DATE_LOCK_FIELD'),
			)),
			'WF_COMMENTS' => new MainEntityTextField('WF_COMMENTS', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_WF_COMMENTS_FIELD'),
			)),
			'IN_SECTIONS' => new MainEntityBooleanField('IN_SECTIONS', array(
				'values' => array('N', 'Y'),
				'title' => Loc::getMessage('ELEMENT_ENTITY_IN_SECTIONS_FIELD'),
			)),
			'XML_ID' => new MainEntityStringField('XML_ID', array(
				'validation' => array(__CLASS__, 'validateXmlId'),
				'title' => Loc::getMessage('ELEMENT_ENTITY_XML_ID_FIELD'),
			)),
			'CODE' => new MainEntityStringField('CODE', array(
				'validation' => array(__CLASS__, 'validateCode'),
				'title' => Loc::getMessage('ELEMENT_ENTITY_CODE_FIELD'),
			)),
			'TAGS' => new MainEntityStringField('TAGS', array(
				'validation' => array(__CLASS__, 'validateTags'),
				'title' => Loc::getMessage('ELEMENT_ENTITY_TAGS_FIELD'),
			)),
			'TMP_ID' => new MainEntityStringField('TMP_ID', array(
				'validation' => array(__CLASS__, 'validateTmpId'),
				'title' => Loc::getMessage('ELEMENT_ENTITY_TMP_ID_FIELD'),
			)),
			'SHOW_COUNTER' => new MainEntityIntegerField('SHOW_COUNTER', array(
				'default_value' => 0,
				'title' => Loc::getMessage('ELEMENT_ENTITY_SHOW_COUNTER_FIELD'),
			)),
			'SHOW_COUNTER_START' => new MainEntityDatetimeField('SHOW_COUNTER_START', array(
				'title' => Loc::getMessage('ELEMENT_ENTITY_SHOW_COUNTER_START_FIELD'),
			)),
			'PREVIEW_PICTURE_FILE' => new MainEntityReferenceField(
				'PREVIEW_PICTURE_FILE',
				'BitrixFileFile',
				array('=this.PREVIEW_PICTURE' => 'ref.ID'),
				array('join_type' => 'LEFT')
			),
			'DETAIL_PICTURE_FILE' => new MainEntityReferenceField(
				'DETAIL_PICTURE_FILE',
				'BitrixFileFile',
				array('=this.DETAIL_PICTURE' => 'ref.ID'),
				array('join_type' => 'LEFT')
			),
			'IBLOCK' => new MainEntityReferenceField(
				'IBLOCK',
				'BitrixIblockIblock',
				array('=this.IBLOCK_ID' => 'ref.ID'),
				array('join_type' => 'LEFT')
			),
			'WF_PARENT_ELEMENT' => new MainEntityReferenceField(
				'WF_PARENT_ELEMENT',
				'BitrixIblockElement',
				array('=this.WF_PARENT_ELEMENT_ID' => 'ref.ID'),
				array('join_type' => 'LEFT')
			),
			'IBLOCK_SECTION' => new MainEntityReferenceField(
				'IBLOCK_SECTION',
				'BitrixIblockSection',
				array('=this.IBLOCK_SECTION_ID' => 'ref.ID'),
				array('join_type' => 'LEFT')
			),
			'MODIFIED_BY_USER' => new MainEntityReferenceField(
				'MODIFIED_BY_USER',
				'BitrixUserUser',
				array('=this.MODIFIED_BY' => 'ref.ID'),
				array('join_type' => 'LEFT')
			),
			'CREATED_BY_USER' => new MainEntityReferenceField(
				'CREATED_BY_USER',
				'BitrixUserUser',
				array('=this.CREATED_BY' => 'ref.ID'),
				array('join_type' => 'LEFT')
			),
			'WF_LOCKED_BY_USER' => new MainEntityReferenceField(
				'WF_LOCKED_BY_USER',
				'BitrixUserUser',
				array('=this.WF_LOCKED_BY' => 'ref.ID'),
				array('join_type' => 'LEFT')
			),
		);
	}
	/**
	 * Returns validators for NAME field.
	 *
	 * @return array
	 */
	public static function validateName()
	{
		return array(
			new MainEntityValidatorLength(null, 255),
		);
	}

	/**
	 * Returns validators for XML_ID field.
	 *
	 * @return array
	 */
	public static function validateXmlId()
	{
		return array(
			new MainEntityValidatorLength(null, 255),
		);
	}
	/**
	 * Returns validators for CODE field.
	 *
	 * @return array
	 */
	public static function validateCode()
	{
		return array(
			new MainEntityValidatorLength(null, 255),
		);
	}
	/**
	 * Returns validators for TAGS field.
	 *
	 * @return array
	 */
	public static function validateTags()
	{
		return array(
			new MainEntityValidatorLength(null, 255),
		);
	}
	/**
	 * Returns validators for TMP_ID field.
	 *
	 * @return array
	 */
	public static function validateTmpId()
	{
		return array(
			new MainEntityValidatorLength(null, 40),
		);
	}

	/**
	 * Add iblock element.
	 *
	 * @param array $data			Element data.
	 * @return MainEntityAddResult
	 */
	public static function add(array $data)
	{
		$result = new MainEntityAddResult();
		$result->addError(new MainEntityEntityError(
			Loc::getMessage('ELEMENT_ENTITY_MESS_ADD_BLOCKED')
		));
		return $result;
	}

	/**
	 * Updates iblock element by primary key.
	 *
	 * @param mixed $primary		Element primary key.
	 * @param array $data			Element data.
	 * @return MainEntityUpdateResult
	 */
	public static function update($primary, array $data)
	{
		$result = new MainEntityUpdateResult();
		$result->addError(new MainEntityEntityError(
			Loc::getMessage('ELEMENT_ENTITY_MESS_UPDATE_BLOCKED')
		));
		return $result;
	}

	/**
	 * Deletes iblock element by primary key.
	 *
	 * @param mixed $primary		Element primary key.
	 * @return MainEntityDeleteResult
	 */
	public static function delete($primary)
	{
		$result = new MainEntityDeleteResult();
		$result->addError(new MainEntityEntityError(
			Loc::getMessage('ELEMENT_ENTITY_MESS_DELETE_BLOCKED')
		));
		return $result;
	}
}

И пример работы с этой сущностью

//Выборка данных
$dbElements = BitrixIblockElementTable::query()
	->setFilter(['IBLOCK_ID' => CATALOG_IBLOCK_ID, 'ACTIVE' => 'Y'])
	->setSelect(['NAME', 'ID', 'DETAIL_PAGE_URL', 'DATE_ACTIVE_FROM'])
	->addSelect('IBLOCK_SECTION_ID', 'PARENT_SECTION')
	->setLimit(10)
	->addOrder('id', 'DESC')
	->exec();

while ($arElement = $dbElements->fetch()) {
	echo "{$arElement['NAME']} - " . $arElement['DATE_ACTIVE_FROM']->format('d.m.Y H:i:s');
}

//Добавление записи
$addResult = BitrixIblockElementTable::add([
	'NAME' => 'Название нового элемента', 
	'IBLOCK_ID' => CATALOG_IBLOCK_ID
]);
if (!$addResult->isSuccess()) {
	echo implode('<br>' ,$addResult->getErrorMessages());
}

Битрикс очень гордится своим модулем Highload-блоков, который полностью написан с использованием D7.
Раньше у них в качестве хранилища произвольного набора информации были только инфоблоки. Инфоблок, для тех кто не в курсе, это такая сущность, которая в БД хранится как комплекс из нескольких таблиц (1 таблица на «базовые» поля элемента инфоблока и до 2х таблиц на свойства элемента инфоблока). Все базовые поля элементов всех инфоблоков хранятся в одной таблице. Если у вас будет 15 инфоблоков, в каждом из которых будет по 500к элементов, все эти элементы по факту будут находиться в одной таблице. Дополнительные свойства элементов инфоблоков джойнятся из других таблиц. Если это инфоблоки первой версии, то все свойства всех инфоблоков также лежат в одной таблице, а в случае с инфоблоками 2.0 (привет, маркетинг) — свойства каждого инфоблока уже разделены по разным таблицам.
И все это дело естественно очень сильно тормозило уже на относительно небольших наборах данных. 400к элементов в одном инфоблоке уже довольно сильно затормаживают работу админки. Маркетологи в битриксе подумали, и запилили Highload-блоки! Разница в реализации между обычными инфоблоками — минимальная. Теперь для каждого highload-блока создается своя таблица + дополнительно создается еще одна таблица для хранения множественных значений. Обычный подход в создании обычной таблицы в БД они назвали гордым именем highload просто потому, что оно тормозит меньше обычных инфоблоков!
Кроме того, внутри модуля, для того, чтобы он работал согласно D7, классы сущностей генерируются динамически и eval'ятся на каждом хите. Вот такой вот highload.

Посмотреть на это

public static function compileEntity($hlblock)
    {
        global $USER_FIELD_MANAGER;

        // generate entity & data manager
        $fieldsMap = array();

        // add ID
        $fieldsMap['ID'] = array(
            'data_type' => 'integer',
            'primary' => true,
            'autocomplete' => true
        );

        // build datamanager class
        $entity_name = $hlblock['NAME'];
        $entity_data_class = $hlblock['NAME'];

        if (!preg_match('/^[a-z0-9_]+$/i', $entity_data_class))
        {
            throw new MainSystemException(sprintf(
                'Invalid entity name `%s`.', $entity_data_class
            ));
        }

        $entity_data_class .= 'Table';

        if (class_exists($entity_data_class))
        {
            // rebuild if it's already exists
            EntityBase::destroy($entity_data_class);
        }
        else
        {
            $entity_table_name = $hlblock['TABLE_NAME'];

            // make with an empty map
            $eval = '
                class '.$entity_data_class.' extends '.__NAMESPACE__.'DataManager
                {
                    public static function getTableName()
                    {
                        return '.var_export($entity_table_name, true).';
                    }

                    public static function getMap()
                    {
                        return '.var_export($fieldsMap, true).';
                    }

                    public static function getHighloadBlock()
                    {
                        return '.var_export($hlblock, true).';
                    }
                }
            ';

            eval($eval);
        }

        // then configure and attach fields
        /** @var BitrixMainEntityDataManager $entity_data_class */
        $entity = $entity_data_class::getEntity();

        $uFields = $USER_FIELD_MANAGER->getUserFields('HLBLOCK_'.$hlblock['ID']);

        foreach ($uFields as $uField)
        {
            if ($uField['MULTIPLE'] == 'N')
            {
                // just add single field
                $field = $USER_FIELD_MANAGER->getEntityField($uField, $uField['FIELD_NAME']);
                $entity->addField($field);

                foreach ($USER_FIELD_MANAGER->getEntityReferences($uField, $field) as $reference)
                {
                    $entity->addField($reference);
                }
            }
            else
            {
                // build utm entity
                static::compileUtmEntity($entity, $uField);
            }
        }

        return EntityBase::getInstance($entity_name);
    }

Черт бы с ним, но эти самые хайлоад блоки ну никак не могут выступать в роли альтернативы обычным инфоблокам. Оказывается их придумали только для того, чтобы хранить справочные не-иерархичные данные. Кроме того, модуль до сих пор не поддерживает таких нужных функций в админке, как фильтрация по полю типа «Дата», нельзя обозвать сущность HLблока каким-то человекопонятным названием, чтобы администратор каждый раз не пугался при входе на страницу редактирования сущности, к примеру, BrandReference. Все это наводит на мысль, что задумывалось это дело ради альтернативы медленным инфоблокам, но допилить не успели (или не осилили, или пошло в разрез с интересами бизнеса), и в итоге зарелизили полу-готовый функционал как новую фишку, а маркетологи причесали и красиво подали эту идею.

C — Controller, или компонент

Обычный компонент в битриксе можно сравнить с виджетами из Yii. Это некий контейнер, обособленный от всех остальных контейнеров, который принимает на вход какие-то параметры, делает какую-то работу, и с результатом работы подключает вьюху. Разработчики битрикс глубоко убеждены, что те компоненты, которые они предоставляют из коробки, решают большинство задач, которые стоят перед их коллегами. Но, как водится, разработчикам же ничего не нравится всегда, и возможностей стандартных компонентов им всегда «немного» не хватает. Поэтому Битриксоиды решили дать разработчикам возможность модифицировать результат работы компонента… с помощью вьюхи. В директории шаблона компонента можно создать файлик result_modifier.php, в котором можно дополнить результат работы компонента своими данными. И если вы вдруг захотите использовать эти данные в другом шаблоне, вам придется скопипастить этот файлик (ну или заинклюдить этот файл из другого шаблона). Меня всегда мучал вопрос — для чего этот пафос? Почему бы не добавить горку запросов прям в php шаблоне? Разница то невеликая получается.
Что это я про шаблоны в разделе о контроллерах…

В битрикс есть 2 вида компонентов 2.0 (опять привет маркетинг) — обычные и комплексные. Обычный компонент — это виджет. Комплексный компонент — это некий контроллер+роутер, который на основании URL понимает, какую именно страницу с набором виджетов нужно отобразить. Порядок работы примерно такой:

  • в url написано /catalog/bolshaya-zelenaya-shapka.html
  • с помощью mod_rewrite битрикс понимает, что для физического раздела /catalog нужно всегда подключать файл /catalog/index.php
  • комплексный компонент парсит url, и понимает, что нужно подключить детальную страницу товара, назовем ее detail
  • комплексный компонент собирает параметры, которые необходимы для работы его дочерних компонентов
  • комплексный компонент подключает свой шаблон detail.php, внутри которого прописано подключение дочерних обычных компонентов

С виду не очень красиво, но работать можно. Однако не все так просто… Если вы с помощью визуального редактора поменяете параметры комплексного компонента, то файлик с настройками адресации (urlrewrite.php) будет перезаписан системой. Причем, если вы вдруг что-то неправильно там написали для других страниц обязательно что-то сломается без какого-либо предупреждения. На практике это может привести к потере работоспособности целых разделов сайта.
Настройка параметров комплексного компонента может превратиться в муку. У одного такого компонента с легкостью может быть сотня входных параметров, просто потому, что нужно настраивать параметры дочерних компонентов.
Комплексный компонент — он вроде бы и роутер. Однако все те маршруты, которые вы создадите в этом компоненте, не попадут в автоматически генерируемый sitemap.xml. Эти ссылки не попадут в модуль поиска. У вас не будет никакой возможности сгенерировать адрес до маршрута извне (например, вы хотите поставить ссылку на детальную страницу бренда где-то в сайдбаре, и нельзя будет обратиться к роутеру с просьбой сгенерить этот URL).

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

Обычные компоненты — это чуть более простые сущности, чем комплексные компоненты. Их задача — принять на вход набор параметров, обработать их, скормить результат работы шаблону и закешировать все и вся.
Вся логика компонента содержится в файле component.php. С 12й версии битрикса (ныне актуальной является версия 16, 4 года прошло) появилась возможность «использовать ООП» в компонентах. Это нововведение заключается в том, что вместо файла component.php можно создать файл class.php, в котором можно вместо обычной лапши написать класс, унаследованный от CBitrixComponent. И это был большой шаг вперед, т.к. появилась возможность наследовать компоненты и не использовать result_modifier.php вообще, и не практиковать копипасту, если нужно вдруг сильно кастомизировать компонент. Но и тут до сих пор все не так ладно. Из всего набора компонентов, лишь процентов 25-30 могут похвастать наличием класса в своем арсенале. Причем добрая половина из них просто не даст вам возможности расширить себя полностью, т.к. написаны они часто нелогично.
К слову, добрые люди пытаются стандартизировать, как-то помочь разработчикам в написании компонентов, и есть соответствующий инструментарий

V — View, или шаблоны

Шаблоны в битриксе можно разделить на несколько типов:

  • Шаблоны обычных и комплексных компонентов 2.0
  • Шаблоны сайта
  • Шаблоны прочих сущностей (почтовых отправлений, выпусков рассылок, веб-форм, генераторов экспортов и еще много чего)

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

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

$APPLICATION->IncludeComponent('bitrix:catalog.section', 'template_name', []);

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

  • bitrix/components/bitrix/catalog.section/templates/template_name
  • local/components/bitrix/catalog.section/templates/template_name
  • bitrix/templates/.default/components/bitrix/catalog.section/template_name
  • bitrix/templates/site_template/components/bitrix/catalog.section/template_name
  • local/templates/.default/components/bitrix/catalog.section/template_name
  • local/templates/site_template/components/bitrix/catalog.section/template_name
  • bitrix/components/bitrix/catalog/templates/.default/bitrix/catalog.section/template_name
  • local/templates/site_template/components/bitrix/catalog/.default/bitrix/catalog.section/template_name

И это еще я не все варианты перечислил…

Шаблон сайта можно рассматривать как набор файлов: header.php, footer.php (да, у сайта обязательно они должны быть), description.php (системное описание шаблона сайта), template_styles.css (стили шаблона сайта), директорию с шаблонами компонентов и еще группка менее значимых файликов. И все. И никак на это не повлиять, ничего с этим не сделать. Невозможно подцепить шаблонизатор.

Про другие шаблоны и говорить нечего. Они либо просто хранятся в БД в виде верстки со включением в нее каких-то «переменных» данных, либо это тупой php файл, который делает всю работу, от выборки параметров из БД до вывода информации. Для примера, можно посмотреть на генератор YML файла для маркета. Нет никакого смысла выкладывать его сюда, просто потому, что он достаточно большой, около 2к строк. Кому нужно, тот нагуглит, лежит он в /bitrix/modules/catalog/load/yandex_run.php

Файловая природа

Как стало ясно выше, в битриксе с архитектурой все не очень хорошо. Но есть у битрикса и еще один важный аспект архитектуры.
Битрикс — это на половину файловая CMS. Очень многие вещи управляются с помощью каких-то файлов:

  • Нужна страница — создай файл
  • Нужен набор страниц — создай файл и подключи там компонент, работающий с инфоблоками
  • Нужно задать title для страницы — отредактируй файл
  • Нужно задать title для всех страниц раздела — создай специальный файл .section.php в корне этого раздела
  • Нужно отредактировать права — редактируй файл .access.php
  • Настройки до инициализации системы — в файле dbconn.php, .settings.php и .settings_extra.php
  • result_modifier.php, component_epilog.php, init.php, .parameters.php, .description.php ....

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

В дополнение к вышесказанному

Можно бесконечно жаловаться на то, как все плохо устроено в битриксе. На мой взгляд, все эти жалобы можно охарактеризовать одним словосочетанием — «как-то не до конца». И действительно, если вдруг битриксоиды анонсируют какую-то фишку, то они ее релизят как-то не полностью, не доделывают, не доводят до ума. Примеров — масса:

  • внедрили ORM — не доделали, пользоваться в полной мере нельзя
  • сделали автозагрузчик, он работает только в модулях, и не по стандартам
  • дали возможность подключить шаблонизатор, но использовать его можно не везде, и не полностью
  • и т.д. и т.п.

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

Админка

Если кто-то работал с админкой, создавал свои страницы в административной части так, как это предлагает делать битрикс, тот меня поймет. Это просто ад. Для тех, кто не в курсе — битрикс предлагает для каждой страницы использовать файл с лапшой. Например, страница детального просмотра заказа в админке в исполнении разработчиков битрикса занимает over 4к строк. У меня IDE начинает подтормаживать при просмотре содержимого этого файла. Там тебе и php, и js, и html. Хорошо хоть, от SQL избавились, хотя я уверен, что на других административных страницах он есть.
И что мешало сделать работу административных страниц с помощью тех же компонентов — не понятно. Кастомизировать большинство административных страниц просто нет никакой возможности. В случае с компонентами это можно было бы сделать в два счета.
К слову, добрые люди сделали модуль, который поможет вам в построении административных страниц

js-фреймворк

В битриксе есть js составляющая, которая выполняет роль некоего клиентского фреймворка. Никто из разработчиков не любит его по нескольким причинам:

  • он почти не документирован
  • он монструозен
  • он во многом дублирует привычный многим jquery

Битрикс очень часто использует его в своих компонентах, тем самым вызывая еще больше гнева разработчиков. Ядро этой библиотеки в минифицированном виде составляет 85кб, что очень не мало. Избежать его подключения не получится, если вы хотите использовать все возможности битрикса (композит, asset-management).

Дух копипасты

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

Asset-management и CDN

Очень «мне нравится» в битриксе способ управления ресурсами. В принципе, можно зарегистрировать набор определенных «библиотек». Каждая библиотека — это набор css/js файлов, который может зависеть от каких-то других библиотек. Если подключить какую-то библиотеку на страницу, то перед ее подключением будут разрешены все зависимости и все зависимые библиотеки будут вставлены на страницу. Все вроде бы хорошо, только каждый ресурс будет вставлен в виде отдельного файла в тег script или link. И благодаря этому существуют сайты, у которых подключено по 30-50 скриптов и столько же файлов стилей.
Говно-вопрос, сказали в битриксе, и сделали волшебную галочку, которая объединяет все эти файлы в один. И появились сайты, где вместо 50 скриптов стало 2, каждый по 300-500кб. Какое-то время назад это объединение работало с ошибками и объединяло одни и те же ресурсы по нескольку раз, но сейчас вроде бы исправили.
И тут битриксоиды выкрутились — прикрутили возможность выгрузить все ресурсы на CDN сервер. Который вечно отваливается…
Потом появился Google Pagespeed Insights, который рекомендовал опустить все ресурсы в нижнюю часть страницы. И в битриксе опять сделали волшебную галочку, которая тупо опускает все ресурсы в body, если они не помечены специальным атрибутом.
А еще они вместе с коробкой распространяют минифицированные версии своих скриптов, которые подключаются при использовании еще одной волшебной галочки в админке.
В общем, никаких вам scss, никаких TypeScript. Хотите грамотно управлять ресурсами — не используйте встроенную систему битрикса, юзайте webpack, который можно спокойно с битриксом подружить.

Многосайтовость / многоязычность

Это, наверно, самая страшная головная боль разработчика, которая продолжается с момента зарождения продукта. Нельзя просто так взять, и создать многоязычный сайт. А если вам нужен многоязычный каталог с разными ценами и валютами — то это превращается в муку, за которую нужно еще и выложить кругленькую сумму (на покупку доп.лицензии для очередной языковой версии сайта придется раскошелиться).
Если вы создаете многоязычный и многовалютный сайт, то будьте готовы к тому, что битрикс будет очень агрессивно сопротивляться этому. Настройки многосайтовости децентрализованы по всей админке. Каждая сущность в админке имеет свою зависимость от языковой версии сайта. Какие-то сущности могут вообще не поддерживать зависимости от сайта/языка, а какие-то имеют только однозначную привязку к языку, так что придется эту сущность продублировать и потом поддерживать.
В базовом варианте, чтобы заставить инфоблок работать в нескольких языках, вам придется создать дубль этого инфоблока. Но на практике никто этого не делает, и пытается придумать свои способы хранения одной сущности централизованно, разнося ее языкозависимые атрибуты по другим хранилищам.
Нельзя задать дефолтный язык при локализации. Если у вас есть языковая переменная, описывающая какую-то фразу на русском, и этой языковой переменной нет в английском исполнении, то на английском сайте будет показана пустая строка, и никак на это нельзя повлиять (во-многих случаях можно было бы оставить русскую фразу, чтобы не было пустот).

Механизм управления правами

Очень замудрили с этой подсистемой. Часто бывает сложно разобраться, почему ты выдал права на просмотр какой-то сущности, а пользователь не может ими воспользоваться. Например, чтобы дать право на редактирование инфоблока, нужно дать доступ к директории /bitrix/admin, выдать права для конкретного инфоблока и выдать права в главном модуле. Чересчур много операций нужно сделать, чтобы выдать права для одной сущности. А если прав не хватает, то без ковыряния в исходниках никак не получится понять — почему.

Конфигурирование

В битриксе нет централизованного хаба, который бы позволил управлять настройками системы. Настройки опять таки децентрализованы по всей системе. Опции есть в настройках модуля, в настройках компонентов, в COption (будучи не вынесенными в админку). В админке опции одного модуля могут быть разнесены по 3-4м разным страницам, которые находятся в совершенно разных местах. urlrewrite можно править через админку! Теперь еще и .settings и .settings_extra. Иногда совершенно не ясно, какие из них более приоритетны, очень часто не хватает пояснений для опций, непонятны взаимосвязи. Нет никакого нативного способа расшаривать конфигурацию между разработчиками.
Настройки бывают очень нелогичными. Иногда доходит до абсурда… посмотрите компонент бигдаты — разве его сможет настроить неподготовленный человек?

Интеграция с 1С

Это тот пункт в списке фич битрикса, на который клюет достаточно большое количество заказчиков. Битрикс обещает в 2 клика настроить двустороннюю интеграцию сайта с 1С, которая будет мгновенно доставлять контент и документы от одной системы к другой.
Да, оно действительно так и есть, но с несколькими оговорками.
Во-первых, чтобы сделать интеграцию «из коробки» без дополнительных усилий, нужно сделать все именно так, как написано в документации битрикса — построить каталог на сайте по тем правилам, которые предлагает битрикс и построить каталог в 1С, которые требует битрикс. В идеале — создать вообще все с нуля, и тогда может быть, у вас все заработает из коробки.
Во-вторых, Битрикс дружит не со всеми конфигурациями 1С из коробки. Стоит предварительно ознакомиться
В-третьих, идеального мира не бывает. Обычно у заказчика, который хочет сайт, уже есть розничный бизнес, а значит уже есть 1С, которая является огромной мусоркой. И эту мусорку приходится прокидывать на сайт. А чтобы на сайте не получилось такой же мусорки, требуется значительно доработать механизм обмена.
Очень часто требования заказчика сильно расходятся с тем видением продукта, которое сформировано у команды Битрикса, и тогда доработка механизма обмена может быть достаточно дорогой, по трудоемкости сопоставимой с разработкой уникального модуля обмена под конкретный случай.
Поэтому не нужно пытать иллюзий по поводу того, что вам удастся легко интегрировать сайт с 1С. Это все происки маркетологов.

Доработка обмена с 1С — это тоже отдельная тема. За организацию обмена каталогом отвечает класс CIBlockCMLImport.- 5.7к строк. Один из главных методов, который чаще всего требует расширения — CIBlockCMLImport::ImportElement, содержит больше 1к строк. Достаточно раз унаследоваться, пару раз обновить продукт на протяжении длительного времени, и можно получить неработающий обмен с 1С. Поэтому часто разработчики не лезут в этот класс и пытаются как-то влезть в процесс импорта с помощью обработчиков событий. Работать с обработчиками событий в битриксе, особенно в модуле инфоблоков — тоже не очень приятное занятие, хотя бы из-за того, что однотипные события устроены не однородно, а некоторых событий просто не хватает.
В общем с этим дела обстоят также печально, как и ранее.

Несогласованность

Мне порой кажется, что разработчики разных модулей не особенно то общаются между собой. Изучая исходники ядра натыкаешься на очень разнородные решения, которые можно было бы выполнить на одном движке, но они реализованы почему-то по разному.
Для примера можно взять свойства элементов инфоблоков и UserFields. И та и другая сущность по факту является дополнительным полем для другой сущности. Она имеет тип, имеет значение и описание. Значение хранится в отдельной(ых) таблице(ах) БД, имеют примерно схожий интерфейс доступа к данным. Так почему бы не сделать для них одинаковый интерфейс?
Вот в конце марта обновился модуль sale до последней версии, и там тоже обещали произвольные свойства для заказов. Неужели там теперь новый, третий интерфейс для работы со расширенными свойствами сущности?

Битрикс24

Это вообще отдельная тема для разговора. На почве этой системы часто возникает путаница. Есть 2 варианта исполнения Б24 — SaaS и Standlone. Есть маркетплейс для Б24, но в нем содержатся приложения только для SaaS версии! Если у вас коробочная версия, купленная за 200 кусков, вы не сможете поставить такое популярнейшие приложения, как конструктор документов, да и вообще вы не сможете на свой Битрикс24 поставить ни одно приложение из маркетплейса для Битрикс24. Вот такой парадокс.
Вместо этого в вашем Битрикс24 будет доступен маркетплейс от обычной версии. Там решений гораздо больше, но они сконцентрированы в основном вокруг Управления Сайтом, а не Б24.

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

К слову, дорабатывать компоненты в коробочной версии Б24 — та еще задачка. Компоненты, которые генерируют js код, который с помощью ajax обращается к php коду, который в ответ генерирует html+js. Это адовая смесь, в которую очень не хочется погружаться.

Документация

Документация по битриксу отстает от развития продукта на 1-1.5 года. Код очень слабо покрыт phpDoc'ами, и часто комментарий перед классом стоит исключительно «для галочки», будучи автоматически сгенерированным в IDE.
Сам стиль изложения документации в официальных источниках часто бывает слишком «вольным», а содержимое некоторых статей в документации может не иметь никакого отношения к самому битриксу.
Курс разработчика имеет очень много информации, однако формат, в котором разработчика знакомят с возможностями системы, не дает того уровня восприятия, который требуется. Если вы зайдете в Cookbook Symfony, то там все разложено по полочкам, расписаны все необходимые аспекты в зависимости от версии. Тогда как в битриксе курс обучения разработчика содержит непонятно по какому принципу структурированную информацию по старому и новому ядру, которая подается сначала отдельно, а потом вперемешку, от чего у начинающих возникает головная боль.

Организация процесса разработки

Из-за специфичности системы не так уж просто организовать удобный процесс разработки. Не самая свежая версия редакции Бизнес (что была под рукой) после установки занимает, вдумайтесь, почти 530 мегабайт

$ du -s *|sort -nr|cut -f 2-|while read a;do du -hs $a;done
523M	bitrix
204K	upload
 64K	bitrixsetup.php
 56K	desktop_app
 20K	readme.html
 20K	license.html
4,0K	web.config
4,0K	urlrewrite.php
4,0K	readme.php
4,0K	license.php
4,0K	install.config
4,0K	index.php

Из этого объема добрая половина — это бинарники и установочники, которые в общем-то не нужны для версионного контроля. Вообще говоря, принято не версионировать битриксовое ядро. Разработчики Битрикса как бы сами гарантируют целостность ядра, управляют сами зависимостями версий разных модулей при обновлениях. Но это несет в себе сразу, как минимум, один большой минус — невозможно одной командой из версионного контроля развернуть полностью работающий проект, приходится собирать его по частям: исходники ядра доставать из битриксового бекапа, а исходники разработчиков — из git.
С базой тоже все не ладно. Если сами вы можете использовать миграции при разработке, то битрикс накатывает обновления в базу с помощью обычных скриптов, которые вы не можете контролировать. Поэтому при обновлениях все равно придется перекидывать бекапы баз от центрального хоста разработки к другим разработчикам.
Добрые люди, опять же, пилят инструменты, которые помогают это все организовать, но заставить битрикс следовать этим правилам к сожалению до сих пор не удается.
Официально битрикс разрешает иметь 2 копии одного дистрибутива. Один — для продакшена, второй для разработки. Если у вас несколько разработчиков на одном проекте — то вы, как бы, вне закона ) На самом деле, достаточно отрубить машине с битриксом входящие и исходящие подключения с/на www.bitrixsoft.com, и тогда можно наклепать сколь угодно много копий разработки, просто они не смогут самостоятельно обновляться.

Коллеги

И последний вопрос, которого хотелось бы коснуться.
В связи с тем, что битрикс имеет низкий порог вхождения, среди компаний, которые предоставляют услуги на этом рынке очень много неквалифицированных кадров. Мне довелось повидать множество различных проектов за свою карьеру (суммарно более сотни), выполненных на 1С-Битрикс. Могу с уверенностью сказать, что 95% из них были выполнены «тяп-ляп». Очень редко попадались проекты, к разработке которых чувствовался подход, однако это были единицы. Это все очень печально.

Выводы

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

Какие тут можно сделать выводы. Битрикс — крайне сложная система в связи с тем, что имеет непродуманную архитектуру, множество изъянов, которые так и продолжают жить в продукте на протяжении длительного времени. С другой стороны Битрикс — это достаточно простая система, которая для старта требует гораздо меньший уровень квалификации, в отличие от фреймворков.
Поддержка этого продукта — весьма неблагодарное занятие, по сравнению с такими продуктами, как Symfony, Laravel, Yii. Продукт очень любит вставлять палки в колеса как неопытным, так и опытным разработчикам, что, в свою очередь, может отражаться и на стоимости услуг опытных разработчиков под Битрикс.

Жалею ли я о том, что так много времени потратил на работу с этой системой? Скорее да, чем нет. Разумнее было бы потратить это время на изучение чего-то более правильного и более логичного (чем я стараюсь активно заниматься сейчас). Но так уж получилось, что некому было меня направить в правильное русло в начале моего пути.

Если вы — начинающий php разработчик, то предпочтите Битриксу изучение фреймворков, таких как Symfony, Laravel, Yii, ZendFramework. Поверьте, в будущем это с лихвой окупится. Освоив любой из этих фреймворков вам не составит труда в будущем разрабатывать что-то под Битрикс. Если у вас нет выбора, то изучайте Битрикс, но в свободное время лучше все-таки пытаться погрузиться в мир фреймворков, чтобы поставить мозги на место.

Если вы — разработчик со стажем в Битрикс, но без опыта в других фреймворках, то обязательно окунитесь в другой мир, вам откроется очень много новых и полезных знаний, которые помогут вам в написании гораздо более качественных решений под 1С-Битрикс. Старайтесь использовать решения из других фреймворков в своих проектах, благо сделать это совсем несложно благодаря компонентному подходу последних и composer.

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

Автор: mmjurov

Источник

Поделиться новостью

* - обязательные к заполнению поля