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

Что-то много развелось в последнее время статей про минусы битрикса, и их опровержений. Раз уж пошла такая пьянка, то и я добавлю свои 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)"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 .= "\t\t\tINNER JOIN b_iblock_element_prop_s".$iblock_id." FPS".$iPropCnt." ON FPS".$iPropCnt.".IBLOCK_ELEMENT_ID = BE.ID\n";
            $arFilterIBlocks[$iblock_id] = $iblock_id;
        }

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

            if($db_prop["bFullJoin"])
                $sFrom .= "\t\t\tINNER 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 .= "\t\t\tLEFT 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 .= "\t\t\tINNER JOIN ".$strTable." FPV".$i." ON FPV".$i.".IBLOCK_PROPERTY_ID = FP".$db_prop["JOIN"].".ID AND FPV".$i.".IBLOCK_ELEMENT_ID = BE.ID\n";
            else
                $sFrom .= "\t\t\tLEFT JOIN ".$strTable." FPV".$i." ON FPV".$i.".IBLOCK_PROPERTY_ID = FP".$db_prop["JOIN"].".ID AND FPV".$i.".IBLOCK_ELEMENT_ID = BE.ID\n";

            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 .= "\t\t\tINNER 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.".ID\n";
                else
                    $sFrom .= "\t\t\tLEFT 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.".ID\n";
            }
            else
            {
                if($db_prop["bFullJoin"])
                    $sFrom .= "\t\t\tINNER 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.".ID\n";
                else
                    $sFrom .= "\t\t\tLEFT 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.".ID\n";
            }

            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 .= "\t\t\tLEFT 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 .= "\t\t\tLEFT JOIN b_iblock B".$i." ON B".$i.".ID = BE".$i.".IBLOCK_ID\n";

            if($db_prop["bJoinSection"])
                $sFrom .= "\t\t\tLEFT JOIN b_iblock_section BS".$i." ON BS".$i.".ID = BE".$i.".IBLOCK_SECTION_ID\n";

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

        foreach($arJoinProps["BE_FPS"] as $iblock_id => $db_prop)
        {
            $sFrom .= "\t\t\tLEFT JOIN b_iblock_element_prop_s".$iblock_id." JFPS".$db_prop["CNT"]." ON JFPS".$db_prop["CNT"].".IBLOCK_ELEMENT_ID = BE".$db_prop["JOIN"].".ID\n";

            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 .= "\t\t\tINNER 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 .= "\t\t\tLEFT 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 .= "\t\t\tINNER 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"].".ID\n";
            else
                $sFrom .= "\t\t\tLEFT 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"].".ID\n";

            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 .= "\t\t\tINNER 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.".ID\n";
                else
                    $sFrom .= "\t\t\tLEFT 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.".ID\n";
            }
            else
            {
                if($db_prop["bFullJoin"])
                    $sFrom .= "\t\t\tINNER 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.".ID\n";
                else
                    $sFrom .= "\t\t\tLEFT 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.".ID\n";
            }

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

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

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

        if($arJoinProps["RV"])
            $sFrom .= "\t\t\tLEFT JOIN b_rating_voting RV ON RV.ENTITY_TYPE_ID = 'IBLOCK_ELEMENT' AND RV.ENTITY_ID = BE.ID\n";
        if($arJoinProps["RVU"])
            $sFrom .= "\t\t\tLEFT 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 .= "\t\t\t".($arJoinProps["RVV"]["bFullJoin"]? "INNER": "LEFT")." JOIN b_rating_vote RVV ON RVV.ENTITY_TYPE_ID = 'IBLOCK_ELEMENT' AND RVV.ENTITY_ID = BE.ID\n";

        //******************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", "\n\t\t\tLEFT 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, ", \t\n\r");
        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, "\t\n")
            .(in_array("USER_NAME", $arSelectFields)? "\t\t\tLEFT JOIN b_user U ON U.ID=BE.MODIFIED_BY\n": "")
            .(in_array("LOCKED_USER_NAME", $arSelectFields)? "\t\t\tLEFT JOIN b_user UL ON UL.ID=BE.WF_LOCKED_BY\n": "")
            .(in_array("CREATED_USER_NAME", $arSelectFields) || in_array("CREATED_BY_FORMATTED", $arSelectFields)? "\t\t\tLEFT JOIN b_user UC ON UC.ID=BE.CREATED_BY\n": "")."
        ";

        $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__."
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__."
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 из нового ядра — Bitrix\Iblock\ElementTable: getList, где корневое пространство имен — это имя вендора, а следующее — имя модуля. Для того, чтобы это работало, в битриксе написали свой автозагрузчик \Bitrix\Main\Loader: 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:

  • \Bitrix\Main\Application: getInstance — инстанс приложения
  • \Bitrix\Main\Config\Configuration: getInstance — инстанс класса для управления конфигами
  • \Bitrix\Main\Page\Asset: getInstance — инстанс Asset-менеджера
  • \Bitrix\Main\EventManager: getInstance — менеджер событий

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

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

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

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

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

 * 
  • ID int mandatory *
  • TIMESTAMP_X datetime optional *
  • MODIFIED_BY int optional *
  • DATE_CREATE datetime optional *
  • CREATED_BY int optional *
  • IBLOCK_ID int mandatory *
  • IBLOCK_SECTION_ID int optional *
  • ACTIVE bool optional default 'Y' *
  • ACTIVE_FROM datetime optional *
  • ACTIVE_TO datetime optional *
  • SORT int optional default 500 *
  • NAME string(255) mandatory *
  • PREVIEW_PICTURE int optional *
  • PREVIEW_TEXT string optional *
  • PREVIEW_TEXT_TYPE enum ('text', 'html') optional default 'text' *
  • DETAIL_PICTURE int optional *
  • DETAIL_TEXT string optional *
  • DETAIL_TEXT_TYPE enum ('text', 'html') optional default 'text' *
  • SEARCHABLE_CONTENT string optional *
  • WF_STATUS_ID int optional default 1 *
  • WF_PARENT_ELEMENT_ID int optional *
  • WF_NEW enum ('N', 'Y') optional *
  • WF_LOCKED_BY int optional *
  • WF_DATE_LOCK datetime optional *
  • WF_COMMENTS string optional *
  • IN_SECTIONS bool optional default 'N' *
  • XML_ID string(255) optional *
  • CODE string(255) optional *
  • TAGS string(255) optional *
  • TMP_ID string(40) optional *
  • WF_LAST_HISTORY_ID int optional *
  • SHOW_COUNTER int optional *
  • SHOW_COUNTER_START datetime optional *
  • PREVIEW_PICTURE_FILE reference to {@link \Bitrix\File\FileTable} *
  • DETAIL_PICTURE_FILE reference to {@link \Bitrix\File\FileTable} *
  • IBLOCK reference to {@link \Bitrix\Iblock\IblockTable} *
  • WF_PARENT_ELEMENT reference to {@link \Bitrix\Iblock\IblockElementTable} *
  • IBLOCK_SECTION reference to {@link \Bitrix\Iblock\IblockSectionTable} *
  • MODIFIED_BY_USER reference to {@link \Bitrix\User\UserTable} *
  • CREATED_BY_USER reference to {@link \Bitrix\User\UserTable} *
  • WF_LOCKED_BY_USER reference to {@link \Bitrix\User\UserTable} * * * @package Bitrix\Iblock **/ class ElementTable extends Main\Entity\DataManager { 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 Main\Entity\IntegerField('ID', array( 'primary' => true, 'autocomplete' => true, 'title' => Loc::getMessage('ELEMENT_ENTITY_ID_FIELD'), )), 'TIMESTAMP_X' => new Main\Entity\DatetimeField('TIMESTAMP_X', array( 'default_value' => new Main\Type\DateTime(), 'title' => Loc::getMessage('ELEMENT_ENTITY_TIMESTAMP_X_FIELD'), )), 'MODIFIED_BY' => new Main\Entity\IntegerField('MODIFIED_BY', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_MODIFIED_BY_FIELD'), )), 'DATE_CREATE' => new Main\Entity\DatetimeField('DATE_CREATE', array( 'default_value' => new Main\Type\DateTime(), 'title' => Loc::getMessage('ELEMENT_ENTITY_DATE_CREATE_FIELD'), )), 'CREATED_BY' => new Main\Entity\IntegerField('CREATED_BY', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_CREATED_BY_FIELD'), )), 'IBLOCK_ID' => new Main\Entity\IntegerField('IBLOCK_ID', array( 'required' => true, 'title' => Loc::getMessage('ELEMENT_ENTITY_IBLOCK_ID_FIELD'), )), 'IBLOCK_SECTION_ID' => new Main\Entity\IntegerField('IBLOCK_SECTION_ID', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_IBLOCK_SECTION_ID_FIELD'), )), 'ACTIVE' => new Main\Entity\BooleanField('ACTIVE', array( 'values' => array('N', 'Y'), 'default_value' => 'Y', 'title' => Loc::getMessage('ELEMENT_ENTITY_ACTIVE_FIELD'), )), 'ACTIVE_FROM' => new Main\Entity\DatetimeField('ACTIVE_FROM', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_ACTIVE_FROM_FIELD'), )), 'ACTIVE_TO' => new Main\Entity\DatetimeField('ACTIVE_TO', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_ACTIVE_TO_FIELD'), )), 'SORT' => new Main\Entity\IntegerField('SORT', array( 'default_value' => 500, 'title' => Loc::getMessage('ELEMENT_ENTITY_SORT_FIELD'), )), 'NAME' => new Main\Entity\StringField('NAME', array( 'required' => true, 'validation' => array(__CLASS__, 'validateName'), 'title' => Loc::getMessage('ELEMENT_ENTITY_NAME_FIELD'), )), 'PREVIEW_PICTURE' => new Main\Entity\IntegerField('PREVIEW_PICTURE', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_PREVIEW_PICTURE_FIELD'), )), 'PREVIEW_TEXT' => new Main\Entity\TextField('PREVIEW_TEXT', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_PREVIEW_TEXT_FIELD'), )), 'PREVIEW_TEXT_TYPE' => new Main\Entity\EnumField('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 Main\Entity\IntegerField('DETAIL_PICTURE', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_DETAIL_PICTURE_FIELD'), )), 'DETAIL_TEXT' => new Main\Entity\TextField('DETAIL_TEXT', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_DETAIL_TEXT_FIELD'), )), 'DETAIL_TEXT_TYPE' => new Main\Entity\EnumField('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 Main\Entity\TextField('SEARCHABLE_CONTENT', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_SEARCHABLE_CONTENT_FIELD'), )), 'WF_STATUS_ID' => new Main\Entity\IntegerField('WF_STATUS_ID', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_WF_STATUS_ID_FIELD'), )), 'WF_PARENT_ELEMENT_ID' => new Main\Entity\IntegerField('WF_PARENT_ELEMENT_ID', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_WF_PARENT_ELEMENT_ID_FIELD'), )), 'WF_NEW' => new Main\Entity\EnumField('WF_NEW', array( 'values' => array('N', 'Y'), 'title' => Loc::getMessage('ELEMENT_ENTITY_WF_NEW_FIELD'), )), 'WF_LOCKED_BY' => new Main\Entity\IntegerField('WF_LOCKED_BY', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_WF_LOCKED_BY_FIELD'), )), 'WF_DATE_LOCK' => new Main\Entity\DatetimeField('WF_DATE_LOCK', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_WF_DATE_LOCK_FIELD'), )), 'WF_COMMENTS' => new Main\Entity\TextField('WF_COMMENTS', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_WF_COMMENTS_FIELD'), )), 'IN_SECTIONS' => new Main\Entity\BooleanField('IN_SECTIONS', array( 'values' => array('N', 'Y'), 'title' => Loc::getMessage('ELEMENT_ENTITY_IN_SECTIONS_FIELD'), )), 'XML_ID' => new Main\Entity\StringField('XML_ID', array( 'validation' => array(__CLASS__, 'validateXmlId'), 'title' => Loc::getMessage('ELEMENT_ENTITY_XML_ID_FIELD'), )), 'CODE' => new Main\Entity\StringField('CODE', array( 'validation' => array(__CLASS__, 'validateCode'), 'title' => Loc::getMessage('ELEMENT_ENTITY_CODE_FIELD'), )), 'TAGS' => new Main\Entity\StringField('TAGS', array( 'validation' => array(__CLASS__, 'validateTags'), 'title' => Loc::getMessage('ELEMENT_ENTITY_TAGS_FIELD'), )), 'TMP_ID' => new Main\Entity\StringField('TMP_ID', array( 'validation' => array(__CLASS__, 'validateTmpId'), 'title' => Loc::getMessage('ELEMENT_ENTITY_TMP_ID_FIELD'), )), 'SHOW_COUNTER' => new Main\Entity\IntegerField('SHOW_COUNTER', array( 'default_value' => 0, 'title' => Loc::getMessage('ELEMENT_ENTITY_SHOW_COUNTER_FIELD'), )), 'SHOW_COUNTER_START' => new Main\Entity\DatetimeField('SHOW_COUNTER_START', array( 'title' => Loc::getMessage('ELEMENT_ENTITY_SHOW_COUNTER_START_FIELD'), )), 'PREVIEW_PICTURE_FILE' => new Main\Entity\ReferenceField( 'PREVIEW_PICTURE_FILE', 'Bitrix\File\File', array('=this.PREVIEW_PICTURE' => 'ref.ID'), array('join_type' => 'LEFT') ), 'DETAIL_PICTURE_FILE' => new Main\Entity\ReferenceField( 'DETAIL_PICTURE_FILE', 'Bitrix\File\File', array('=this.DETAIL_PICTURE' => 'ref.ID'), array('join_type' => 'LEFT') ), 'IBLOCK' => new Main\Entity\ReferenceField( 'IBLOCK', 'Bitrix\Iblock\Iblock', array('=this.IBLOCK_ID' => 'ref.ID'), array('join_type' => 'LEFT') ), 'WF_PARENT_ELEMENT' => new Main\Entity\ReferenceField( 'WF_PARENT_ELEMENT', 'Bitrix\Iblock\Element', array('=this.WF_PARENT_ELEMENT_ID' => 'ref.ID'), array('join_type' => 'LEFT') ), 'IBLOCK_SECTION' => new Main\Entity\ReferenceField( 'IBLOCK_SECTION', 'Bitrix\Iblock\Section', array('=this.IBLOCK_SECTION_ID' => 'ref.ID'), array('join_type' => 'LEFT') ), 'MODIFIED_BY_USER' => new Main\Entity\ReferenceField( 'MODIFIED_BY_USER', 'Bitrix\User\User', array('=this.MODIFIED_BY' => 'ref.ID'), array('join_type' => 'LEFT') ), 'CREATED_BY_USER' => new Main\Entity\ReferenceField( 'CREATED_BY_USER', 'Bitrix\User\User', array('=this.CREATED_BY' => 'ref.ID'), array('join_type' => 'LEFT') ), 'WF_LOCKED_BY_USER' => new Main\Entity\ReferenceField( 'WF_LOCKED_BY_USER', 'Bitrix\User\User', 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 Main\Entity\Validator\Length(null, 255), ); } /** * Returns validators for XML_ID field. * * @return array */ public static function validateXmlId() { return array( new Main\Entity\Validator\Length(null, 255), ); } /** * Returns validators for CODE field. * * @return array */ public static function validateCode() { return array( new Main\Entity\Validator\Length(null, 255), ); } /** * Returns validators for TAGS field. * * @return array */ public static function validateTags() { return array( new Main\Entity\Validator\Length(null, 255), ); } /** * Returns validators for TMP_ID field. * * @return array */ public static function validateTmpId() { return array( new Main\Entity\Validator\Length(null, 40), ); } /** * Add iblock element. * * @param array $data Element data. * @return Main\Entity\AddResult */ public static function add(array $data) { $result = new Main\Entity\AddResult(); $result->addError(new Main\Entity\EntityError( 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 Main\Entity\UpdateResult */ public static function update($primary, array $data) { $result = new Main\Entity\UpdateResult(); $result->addError(new Main\Entity\EntityError( Loc::getMessage('ELEMENT_ENTITY_MESS_UPDATE_BLOCKED') )); return $result; } /** * Deletes iblock element by primary key. * * @param mixed $primary Element primary key. * @return Main\Entity\DeleteResult */ public static function delete($primary) { $result = new Main\Entity\DeleteResult(); $result->addError(new Main\Entity\EntityError( Loc::getMessage('ELEMENT_ENTITY_MESS_DELETE_BLOCKED') )); return $result; } }


  • И пример работы с этой сущностью
    //Выборка данных
    $dbElements = Bitrix\Iblock\ElementTable::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 = Bitrix\Iblock\ElementTable::add([
            'NAME' => 'Название нового элемента', 
            'IBLOCK_ID' => CATALOG_IBLOCK_ID
    ]);
    if (!$addResult->isSuccess()) {
            echo implode('
    ' ,$addResult->getErrorMessages()); }

    Битрикс очень гордится своим модулем Highload-блоков, который

    © Habrahabr.ru