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

Opensource Компонент Заказа Bitrix

добавил Шубин Александр 12 Май, 2019


Появилось свободное время и я решил выложить рабочий пример компонента оформления заказа, не просто статью, а проверенный, работающий и функциональный код. За всё время работы с битриксом я довольно много кастомизировал штатный компонент. Написал несколько своих компонентов оформления заказа (первый еще без d7, приблизительно в 2010 году, быстрое оформление во всплывающем окне). Видел варианты реализации от других разработчиков. В общем смею надеяться некоторый опыт в оформлении заказа у меня есть. И весь свой опыт я постарался вложить в новый универсальный компонент.

Вот ссылка на github https://github.com/alorian/bxorder. Это не обрезанный код с существующего проекта. Компонент я писал с нуля, сразу с прицелом на универсальность.

Для начала давайте поговорим о смысле жизни. Зачем это всё вообще надо?

TL;DR. Ставим компонент из композера, архивом с гитхаба или из маркетплейс. Пишем собственный шаблон компонента. В шаблоне достаточно сформировать обычную форму с пятью переменными. Компонент отдает в шаблон объект заказа и коллекцию ошибок, превращаем объекты в массив, в файле result_modifier и формируем шаблон как обычно. По необходимости используем готовый аякс для поиска местоположений, расчета доставки или сохранения заказа. Под конкретный проект если не хватает функционала дописываем нужное на событиях или через наследование компонента.

Зачем нужен компонент?

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

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

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

На мой взгляд сразу стоит отказаться от варианта с доработкой дефолтного шаблона, от правки html/js. Когда вы захотите это сделать, вы попросту потеряетесь в тысячах строк готового кода. Каждая правка будет вызывать длительную фрустрацию и желание от всего отказаться. Не исключено, что вы доживёте (или уже дожили) до стадии принятия и всё станет хорошо, в этом случае вас остается только поздравить, но большинство останавливается на стадии гнева.

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

Однако же если вам нужна простая и лаконичная форма заказа, или более сложная, или даже просто другая, то проще отказаться от штатного шаблона вообще. И возможно от штатного компонента тоже.

Что можно улучшить?

Для понимания масштаба, весь фреймворк symfony в минимальной комплектации это 13к строк в php файлах. Папка с компонентом sale.order.ajax это 11к строк в php файлах и 18,5к строк в js файлах. Эти цифры не для того чтобы кого-то обвинить, совсем нет. По ним вы можете грубо прикинуть количество времени необходимое для изучения темы.

Для начала подумаем о том какой функционал реализует штатный компонент. Почему он такой сложный. И в процессе анализа можно обнаружить, что оформление заказа это довольно простое действие, а значительная часть сложности возникает из-за стороннего никак напрямую не связанного с оформлением функционала. Если бегло пройтись по функционалу, то помимо самого оформления штатный компонент авторизует старых пользователей (да, это прямо в коде компонента), регистрирует новых, работает с профилями покупателей, применяет скидочные купоны. Это всё полито толстым таким слоем галочек из параметров, обработкой событий и набором аякс методов. Тщательно размешиваем и получаем идеальный хаос 🙂

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

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

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

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

Кристально ясное оформление

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

А что люди реально хотят делать с объектом заказа? Вариантов миллион. Яблоко можно просто съесть, сделать компот, сделать повидло, фруктовый салат, можно достать семена и посадить яблоню. И если мы в самом компоненте будем формировать массив $arResult, который пригодится вообще во всех возможных случаях, то мы опять придем к тому, что компонент разрастется и перестанет быть доступным для понимания. Это список новостей всегда будет выглядеть примерно одинаково, а в процедуре оформления заказа фантазия может разгуляться. Отсюда первый вывод — для универсальности мы должны полностью отказаться от массива $arResult в компоненте. Компонент на выходе должен отдавать только объект заказа.

Но ведь $arResult реально удобен и в шаблоне компонента гораздо понятнее выглядит строка «echo $arResult[‘SUM’]», чем вызов «SaleFormatCurrency($order->getPrice(), $order->getCurrency())». Ничего страшного во втором варианте нет, однако же первый выглядит проще и лаконичнее, а мы боремся за кристальную ясность. В структуре компонента битрикс есть идеальное место для формирования массива $arResult — это файл result_modifier.php. Отсюда второй вывод, чтобы упростить понимание процедуры заказа мы должны формировать массив $arResult до старта шаблона компонента, а в шаблоне уже пользоваться заранее подготовленным массивом.

Что еще не учтено? Обработка ошибок. В процессе заполнения объекта заказа компонент может сгенерировать какие либо ошибки, которые мы пока что никак не передаем в шаблон. В объекте заказа они не хранятся, от $arResult мы отказались. Поэтому все же придется чуть усложнить концепт, компонент оформления заказа должен возвращать не только объект заказа, но и коллекцию ошибок. В битриксе для коллекции ошибок опять таки есть готовая реализация, это класс «Bitrix\Main\ErrorCollection». Сами ошибки внутри хранятся тоже в виде объектов.

Расставим всё по полкам в последний раз. В коде компонента мы формируем два публичных объекта — объект заказа и коллекцию ошибок. Далее из этих двух объектов в result_modifier мы собираем массив $arResult. И уже в шаблоне формируем с помощью $arResult итоговый html+js+css.

Стартовое состояние

Важной частью штатного sale.order.ajax является работа с профилями. Что такое профиль покупателя, если смотреть в самую суть? Это только стартовое состояние формы заказа. Если мы переключаем профиль, то стартовое состояние формы (заполненные значения в полях) меняется. При этом любые исправления стартового состояния имеют безусловный приоритет, если пользователь поменял текст в каком то поле, то значение из профиля мы должны игнорировать.

Таким образом что должен делать блок работы с профилями? Где-то выше компонента оформления заказа он должен сформировать массив со стартовыми полями заказа. И далее передать этот массив в компонент.

Как мы можем красиво передать данные в компонент? Для этого опять таки есть хороший штатный вариант — параметры компонента. Именно поэтому при отсутствии данных в http post запросе, объект заказа наполняется данными из параметров компонента.

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

Работа с пользователями

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

На текущий момент опенсорный компонент оформления заказа вообще никак не взаимодействует с пользователями. Если в момент сохранения заказа пользователь не авторизован, то сам битрикс (не компонент) вернет ошибку «Не заполнено обязательное поле «USER_ID»». Таким образом сразу после установки компонент может работать только по второму сценарию, когда пользователи нам очень важны.

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

Как установить?

Установка доступна с помощью композера, через маркетплейс битрикс и просто вручную, архивом.

Установка с маркетплейс

Чтобы воспользоваться компонентом без участия композера, перейдите по ссылке на маркетплейс битрикса — http://marketplace.1c-bitrix.ru/solutions/opensource.order/. И установите решение как обычно (если там пусто, то возможно я его еще не подготовил, либо оно еще на модерации).

Установка через composer

Для начала пропишите в своем composer.json путь до папки bitrix. Обратите внимание на блок extra:

{
    "name": "your/project",
    "authors": [
        {
            "name": "Alexander Shubin",
            "email": "alorian@yandex.ru"
        }
    ],
    "require": {},
    "extra": {
        "bitrix-dir": "./"
    }
}

Путь до папки bitrix нужно прописывать относительно файла composer.json. Например если файл composer.json лежит в /local/libs,
то нужно прописать «bitrix-dir»: «../../bitrix». По дефолту установщик считает, что файл composer.json лежит в document_root.
Если не указать корректный bitrix-dir, то будет создана папка bitrix/modules/opensource.order/ рядом с composer.json.

После того как прописали правильный bitrix-dir выполните:

$ composer require alorian/bxorder

После выполнения команды откройте список модулей маркетплейс в админке /bitrix/admin/partner_modules.php?lang=ru, если
bitrix-dir был указан корректно, то вы увидите строку с модулем opensource.order. Нажмите «Установить» в выпадающем меню.

Ручная установка

Скачайте архив https://github.com/alorian/bxorder/archive/master.zip и самостоятельно распакуйте его содержимое в папку модулей битрикса — /bitrix/modules, либо /local/modules.

В папке модулей у вас должна быть папка opensource.order, а не bxorder-master, папку bxorder-master которая лежит
в архиве необходимо переименовать. Таким образом полный путь до файла include.php у вас должен быть /bitrix/modules/opensource.order/include.php, либо /local/modules/opensource.order/include.php

После распаковки архива откройте список модулей маркетплейс в админке /bitrix/admin/partner_modules.php?lang=ru,
найдите строку с модулем opensource.order и нажмите «Установить» в выпадающем меню

Добавление на страницу

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

Размещение компонента оформления заказа на странице

После этого, даже без каких либо дополнительных настроек вы увидите дефолтный демонстрационный шаблон:

дефолтный демонстрационный шаблон

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

Как использовать?

Что вам нужно сделать как программисту для интеграции верстки? В самом простом случае вам всего лишь нужно сформировать форму (html тэг <form>) в шаблоне компонента, которая при отправке передаст на сервер пять переменных:

  1. person_type_id. Переменная которая содержит тип плательщика.
  2. properties[]. Массив переменных со свойствами заказа. Например, если у свойства символьный код — FIO, то атрибут name у инпута ставьте properties[FIO]. Если переменная множественная то ставьте name=properties[FIO][]
  3. delivery_id. В самом простом случае это просто input типа radio, у которого атрибут name=delivery_id
  4. pay_system_id. Так же как и с доставкой, просто radio инпут, только атрибут name=pay_system_id
  5. save. Если переменная save=y, то компонент сохранит заказ. Во всех остальных случаях компонент просто обновит данные в объекте заказа и отдаст шаблон.

Да, всё настолько просто. Формируете форму с этими пятью переменными и вы великолепны. Это далеко не всё что можно сделать с помощью опенсорсного компонента. Но даже в самых сложных шаблонах суть останется прежней. Оформление заказа это не магия, просто обычная форма в браузере, просто чуть больше полей чем в обратной связи.

Базис

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

Рассматривайте каждую из частей как черный ящик. С одной стороны запихиваем в ящик какие то данные, трясем, и на выходе с другой стороны получаем нечто новое. У ящика есть вход и выход.

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

Файл class.php компонента

Именно с этого файла начинается работа всего компонента. Основная цель этого файла — создать объект заказа и передать его дальше.

Массив $arResult вообще не используется. Компонент в результате своей работы формирует всего две публичные переменные — переменную $this->order с объектом заказа и переменную $this->errorCollection с ошибками.

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

Итого. На вход компоненту мы подаем массив параметров компонента и массив данных из http запроса. На выходе из компонента мы получаем объект заказа и коллекцию ошибок.

Файл result_modifier.php

Основная цель этого файла — сформировать массив $arResult и передать этот массив в шаблон.

На выходе из компонента мы получили объект заказа. В принципе можно сразу в шаблоне компонента пользоваться методами этого объекта, без формирования $arResult вообще. Однако же с предварительно сформированным $arResult, в шаблоне код получается более чистым и понятным. Помимо чистоты кода массив $arResult вам понадобится для использования во vue.js или react.js, так как для передачи на фронт переменные в любом случае придется доставать из объектов.

На вход в result_modifier.php мы получаем объект заказа и коллекцию ошибок, на выходе мы отдаем $arResult с данными для шаблона.

Шаблон компонента

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

На вход шаблона мы получаем массив $arResult, на выходе шаблон отдает готовый html код, плюс скрипты и стили если это необходимо. Если вы используете webpack+vue/react, то в шаблоне просто передаем массив $arResult в браузер через глобальный window.

Куда отправлять аякс запросы?

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

Аякс работает с помощью нового механизма аякс, о котором я писал в статье по ссылке. В простом случае никакие аякс запросы вам не понадобятся. Достаточно просто сформировать html форму без скриптов вообще и отправить ее по кнопке submit на эту же страницу. Однако для чуть более сложных сценариев вы можете воспользоваться следующими готовыми методами:

Поиск местоположений

Файл ajax.php в папке компонента. Метод searchLocationAction в классе OpenSourceOrderAjaxController. Сам поиск осуществляется с помощью штатного метода \Bitrix\Sale\Location\Search\Finder::find. Далее по каждому из найденных местоположений собирается полная информация, в том числе путь от корня — страна, регион, область, и т.д. Можно получить эту же информацию в обратном порядке — местоположение, область, регион, страна.

На вход метод принимает четыре параметра:

1. Поисковая строка string $q, любая фраза по которой осуществляется поиск. Например при вводе «санкт» битрикс с большой долей вероятности найдет «Санкт-Петербург».

2. Количество поисковых результатов, int $limit = 5. Сколько найденных местоположений вам придет в ответе. В коде стоит проверка — не меньше одного местоположения и не больше пятидесяти.

3. Тип местоположений, string $typeCode. Если вам нужны только города, то передайте аргумент $typeCode = ‘CITY’. По умолчанию поиск осуществляется вообще по всем типам местоположений.

4. Исключение каких то частей и найденного названия, array $excludeParts = []. Если в названии найденного местоположения вам не нужна страна и округ, то просто передайте в этот аргумент значение [COUNTRY, COUNTRY_DISTRICT] и в ответе вам придет название местоположения начиная с региона.

Всего в битриксе есть семь дефолтных типов местоположений. Любое местоположения которое хранится штатно в таблице местоположений будет принадлежать одному из семи типов — COUNTRY, COUNTRY_DISTRICT, REGION, SUBREGION, CITY, VILLAGE, STREET.

5. Порядок формирования имени, string $sortOrder = ‘desc’. Как я говорил выше, найденное местоположение может быть отдано в формате страна, округ, регион и т.д., а может быть наоборот в формате регион, округ, страна. В этом параметре вы можете указать нужный порядок.

Возвращает массив найденных местоположений:

Array
(
    [0] => Array
        (
            [name] => Москва
            [code] => 0000073738
            [type] => CITY
            [type_name] => Город
            [label] => Москва, Центр, Россия
            [parts] => Array
                (
                    [CITY] => Array
                        (
                            [name] => Москва
                            [code] => 0000073738
                            [type] => CITY
                            [type_name] => Город
                        )

                    [COUNTRY_DISTRICT] => Array
                        (
                            [name] => Центр
                            [code] => 0000028024
                            [type] => COUNTRY_DISTRICT
                            [type_name] => Округ
                        )

                    [COUNTRY] => Array
                        (
                            [name] => Россия
                            [code] => 0000028023
                            [type] => COUNTRY
                            [type_name] => Страна
                        )

                )

        )

        [...]     
)

Расчет стоимости доставки

Файл ajax.php в папке компонента. Метод calculateDeliveriesAction в классе OpenSourceOrderAjaxController. В методе как обычно создается объект заказа и отгрузка. По отгрузке выбираются все доступные доставки и рассчитывается стоимость этих доставок.

На вход принимает три параметра:

1. Тип плательщика, int $person_type_id.

2. Массив свойств, array $properties. В массиве должно быть как минимум одно свойство — свойство с местоположением. Иначе доставки ничего не смогут рассчитать. Лучше всего передавать вообще все свойства из формы заказа.

3. Массив ID доставок, array $delivery_ids. Может быть пустым, но если задан, то будут рассчитаны доставки только с указанными ID. Доставки в любом случае должны быть доступны для заказа.

Возвращает массив с рассчитанной стоимостью доставок:

Array
(
    [2] => Array
        (
            [id] => 2
            [success] => 1
            [name] => Доставка курьером
            [logo_path] => /upload/sale/delivery/logotip/789/78984a7a84ff245422e5abd911c1972a.png
            [period] => 
            [base_price] => 500
            [base_price_display] => 500 руб.
            [price] => 350
            [price_display] => 350 руб.
            [discount] => 150
            [discount_display] => 150 руб.
            [errors] => Array
                (
                )

        )
        
        [...]
)

Сохранение заказа

Файл ajax.php в папке компонента. Метод saveOrderAction в классе OpenSourceOrderAjaxController. По сути вызываются те же самые методы основного компонента, что и при отправке формы на страницу. Полный аналог executeComponent, за исключением вывода шаблона. Результат сохранения заказа, либо ошибки отдаются в виде чистых данных.

Демонстрационные шаблоны

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

Дефолтный шаблон

Скрин с дефолтной формой заказа я приводил в начале статьи. Там есть пять основных блоков:

1. Таблица со списком свойств. В шаблоне реализовано отображение свойств типа строка, чекбокс Да/Нет, выбор из списка значений, заполнение даты и поиск местоположений.

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

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

3. Выбор платежной системы. Блок из обычных радио инпутов плюс вывод списка ошибок связанных с оплатой.

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

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

Переключение типа плательщика

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

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

Обновление списка доставок

Это тот же самый дефолтный шаблон, но в файле script.js шаблона добавлен обработчик на изменение свойств типа location. При изменении свойства с типом location отправляется аякс запрос на расчет стоимости доставки. После получения результата расчета текущий список доставок очищается и строится заново по данным пришедшим из аякс.

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

Что делать если не хватает функционала?

Если вы считаете, что не хватает какого-то общего функционала, то вы можете написать его самостоятельно и отправить pull request. Кратко для тех кто не сталкивался с опенсорсом. Вы делаете форк репозитория, в своем форке добавляете функционал и отправляете pull request из форка в оригинал. Я увижу что именно вы добавили, если код хорошо написан и будет полезен многим, а не только вам, то этот pull request будет влит в основную ветку.

Если вы хотите написать какой то функционал под конкретный проект, то у вас есть две пути — события и наследование компонента.

Для начала про события. Допустим вы хотите отображать форму вообще всем пользователям даже без авторизации, и регистрировать пользователя в момент сохранения заказа. Можете использовать обработчик перед сохранением заказа OnSaleOrderBeforeSaved. В этот обработчик вы получаете объект заказа. Если это новый заказ ($order->isNew()), то регистрируете пользователя как вам хочется, а потом подставляете ID только что созданного пользователя в заказ через $order->setFieldNoDemand(‘USER_ID’, $userId).

В OnSaleOrderBeforeSaved можно добавить любые проверки по данным заказа и возвращать ошибки. Эти ошибки будут добавлены в общую коллекцию ошибок и переданы в шаблон.

Второй путь — наследование компонента. Вы можете прочитать об этом в документации. Допустим вы считаете, что проверка свойств слишком простая. Наследуете компонент и перекрываете метод validateProperties() в классе компонента. У вас будет новый класс компонента всего с одним методом:

<?php

use Bitrix\Main\Result;

CBitrixComponent::includeComponentClass('opensource:order');

class InheritedOpenSourceOrderComponent extends OpenSourceOrderComponent
{
    /**
     * @return Result
     *
     * @throws Exception
     */
    public function validateProperties()
    {
        $parentResult = parent::validateProperties();
        
        $result = new Result();
        
        //добавляем свою логику
        
        if (!$parentResult->isSuccess()) {
            $result->addErrors($parentResult->getErrors());
        }

        return $result;
    }

}

Аналогичным образом можно переписать любой метод компонента. Можно даже не вызывать родительский метод, если ваша логика отличается от дефолтной. Не обязательно только переписывать какие то методы, можно добавлять новые.

Дальнейшие планы

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

Сам компонент будет развиваться. Есть некоторые вещи которые я хочу улучшить. Кардинально ничего не поменяется, вы всё так же можете рассчитывать на объект заказа и коллекцию ошибок в шаблоне.

А так в целом, жду пул реквесты и просто текстовые предложения по улучшению функционала в комментариях к этой статье или в issues на гитхабе https://github.com/alorian/bxorder/.


добавил Шубин Александр 12 Май, 2019
Рубрика: bitrix


66 комментариев

  • Александр:

    Большое вам спасибо за такую большую работу.

    Сразу же после установки появилась ошибка [Error]
    Call to undefined method Bitrix\Sale\Order::getDeliveryIdList() (0)
    /home/bitrix/www/bitrix/components/opensource/order/templates/example_refresh_delivery/result_modifier.php:77
    #0: include
    /home/bitrix/www/bitrix/modules/main/classes/general/component_template.php:868
    #1: CBitrixComponentTemplate->__IncludeMutatorFile(array, array)
    /home/bitrix/www/bitrix/modules/main/classes/general/component_template.php:775
    #2: CBitrixComponentTemplate->IncludeTemplate(array)
    /home/bitrix/www/bitrix/modules/main/classes/general/component.php:681
    #3: CBitrixComponent->showComponentTemplate()
    /home/bitrix/www/bitrix/modules/main/classes/general/component.php:629
    #4: CBitrixComponent->includeComponentTemplate()
    /home/bitrix/www/bitrix/components/opensource/order/class.php:406
    #5: OpenSourceOrderComponent->executeComponent()
    /home/bitrix/www/bitrix/modules/main/classes/general/component.php:585
    #6: CBitrixComponent->includeComponent(string, array, NULL)
    /home/bitrix/www/bitrix/modules/main/classes/general/main.php:1035
    #7: CAllMain->IncludeComponent(string, string, array)
    /home/bitrix/www/personal/order/make/index.php:8

    С чем это может быть связано? Почему метод не подхватывается?

    • Единственное что в голову приходит — старая версия битрикса. То есть метода действительно нет. Других идей нет. Если бы модуля интернет магазин или самого объекта заказа не было бы, то гораздо раньше бы ошибка вылетела.

      Посмотри файл /bitrix/modules/sale/lib/order.php, сам метод если он есть должен быть в этом файле. Если его там действительно нет, то можно его функционал повторить, в нем 10 строк кода всего: https://bxapi.ru/src/?id=484003, не в файле с классом заказа повторить естественно, а в result_modifier сделать то же самое, что и метод делает

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

      • Александр:

        Действительно, проблема была в старой версии битрикса, теперь все ок. Спасибо.

  • Александр:

    Как можно вывести изображение и дополнительные поля в список товаров в заказе в result_modifier.php ?

    • Для начала получаешь ID элементов инфоблока через $basketItem->getProductId() в массив. И далее обычным CIBlockElement::GetList() с фильтром по ID получаешь одним запросом всё что нужно. Любые свойства товаров, картинки и т.д.

  • Александр:

    Как можно добавить поле комментария? Пробовал добавлять инпут с name=»USER_DESCRIPTION» или name=»ORDER_DESCRIPTION».

    • Александр:

      Если кому-то интересно, в класс executeComponent()

      после строк

      if (!empty($propertiesList)) {
      $this->setOrderProperties($propertiesList);
      }

      добавил $this->order->setField(‘USER_DESCRIPTION’, $this->request[‘USER_DESCRIPTION’]);

      Где $this->request[‘USER_DESCRIPTION’] — наше поле комментария с name=»USER_DESCRIPTION»

  • Виктор:

    Добрый день,
    Спасибо за огромную проделанную работу 🙂
    В чем может быть дело если не подставляются города в местоположении: https://imgur.com/s9xEjQx
    В том числе и по AJAX запросу ответ приходит пустой: https://imgur.com/KTWcvhw

    • А в админке эти местоположения есть? Местоположения импортировались? С индексом по местоположениям всё в порядке? Какая версия битрикса?

      • Виктор:

        Да, все местоположения есть, стандартный sale.order.ajax их ищет

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

          • Виктор:

            Проблема оказалось в том что у меня на сайте кодировка windows-1251 (извините, это наследие не от меня…)
            В общем ajax запрос немного по-другому сформировал и все заработало.

  • Виталий:

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

    • Ну в целом это задача фронта отрисовать само всплывающее окошко или просто карту на страницу с пунктами самовывоза. На бэкенде точки самовывоза для каждой доставки по своему получаются, нет какой то одной общей функции «Получить точки самовывоза», которую бы реализовали все доставки. Модули которые добавляют всплывающее окошко в штатном sale.order.ajax по сути вешаются на события компонента и впихивают свой код, который штатно не предусмотрен.

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

      • Виталий:

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

        • В компоненте для этого ничего не собирается, да. Довольно частый кейс — оплата заказа только после подтверждения. Поэтому сразу после оформления кнопка «оплатить» не всем нужна, достаточно вывести текст типа «После подтверждения менеджером вы сможете оплатить заказ».

          Если нужно показать кнопку прямо сразу после оформления, есть несколько вариантов:

          1. Самый логичный вариант — редиректить покупателя куда-то. Например на страницу заказа в истории заказов. Там уже есть кнопка «Оплатить». Человек нажал «Оформить» и тут же оказывается на странице заказа в истории.

          2. Если нужна кнопка перехода к оплате прямо на странице оформления заказа, без редиректа, то ее довольно просто получить:

          $service = \Bitrix\Sale\PaySystem\Manager::getObjectById($paymentItem->getPaymentSystemId());
          if ($service) {
              $context = \Bitrix\Main\Application::getInstance()->getContext();
          
              $result = $service->initiatePay($paymentItem, $context->getRequest(), BaseServiceHandler::STRING);
              if ($result->isSuccess()) {
                  echo $result->getTemplate();
              }
          }

          Переменная $paymentItem это просто оплата, какой то один элемент из коллекции оплат заказа $order->getPaymentCollection().

          То есть буквально несколько строк кода. Метод $service->initiatePay может сразу выводить на экран, а может возвращать строку с html кодом. Здесь в примере именно возврат строки, по дефолту, если третий аргумент метода убрать, то кнопка сразу выведется на экран.

          3. Еще у битрикса были какие-то готовые компоненты, которые выводят кнопку. Можно попробовать компонент bitrix:sale.order.payment
          https://dev.1c-bitrix.ru/user_help/components/magazin/zakaz/sale_order_payment.php
          Просто вызвать его в шаблоне. Главное там глянуть какие переменные он смотрит.

          Реально на практике я пользовался только первым и вторым методом.

          • Виталий:

            Спасибо за наводку.
            Необходим вариант, когда после оформления заказа человек попадает на страницу «Спасибо за заказ, для оплаты нажмите на кнопку» и по кнопки он уходит на платежный шлюз. после оплаты возвращается из шлюза на страницу «Оплата принята, спасибо». То есть первый вариант не подходит. Второй вариант подошел бы, но не понял куда вставлять этот код, если честно. в шаблоне он выдает ошибку.
            Третий вариант я посмотрел и он даже работает, если GETом передавать ORDER_ID=xxx, но почему то у меня не рендерится вся страница при этом. откуда то берется форма с кнопкой, но так и не понял откуда, т.к. у этого компонента нет шаблона в известных мне директорриях 🙁

  • andrey:

    Здравствуйте. Подскажите, как подключить модуль при помощи includeModule (какие параметры нужны, возможно, пример кода), он не появляется в списке в визуальном редакторе.

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

  • Grigory:

    Здравствуйте. Как вы посоветуете быть со сторонними модулями различных служб доставки у которых есть ПВЗ ? Они вешаются на события коих в самописном компоненте нет. Если вас не затруднит подскажите как получить массив пунктов выдачи на примере СДЭК-а или PickPoint-a.

    • Доставка сдэк насколько я понимаю от точки самовывоза не зависит. То есть чтобы отобразить список точек самовывоза достаточно вызвать их виджет. У них есть готовый скрипт который во всплывающем окошке показывает точки самовывоза. Искать нужно по словам «сдэк виджет». Вызываешь их скрипт и слушает клик по маркерам на карте. По клику закрываешь виджет например и ставишь текст в свойство

      По пикпоинту хз. Они в пределах одного города по разным точкам цену разную делают? Если одинаковую, то выбор точки самовывоза это тоже по сути отображение карты и обработчик на клик по маркеру на карте.

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

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

  • Дмитрий:

    Столкнулся с проблемой при получении свойств заказа привязанных к доставке. Если по умолчанию не выбрана доставка или выбран самовывоз, то при выборе доставки курьером не появляются поля с адресом доставки. Как получить свойства привязанные к доставке (если эта доставка не выбрана)? Аналогичная ситуация со связью доставки и оплаты? Как эти данные можно получить через ajax.

  • Афанасий:

    Что делать если не работает смена цены в шаблоне refresher_delivery?

    • Название шаблона неправильно написал. В примерах есть шаблон «example_refresh_delivery». Если что то не работает, смотри консоль браузера, отправляется ли аякс запрос, какие есть ошибки

  • Афанасий:

    template_71be40766ac6a19e32ff3ae89aa269ea_v1.js?1568601850397935:221 Uncaught TypeError: $(…).footable is not a function
    at HTMLDocument. (template_71be40766ac6a19e32ff3ae89aa269ea_v1.js?1568601850397935:221)
    at j (jquery-2.1.3.min.js?156860257384320:2)
    at Object.fireWith [as resolveWith] (jquery-2.1.3.min.js?156860257384320:2)
    at Function.ready (jquery-2.1.3.min.js?156860257384320:2)
    at HTMLDocument.I (jquery-2.1.3.min.js?156860257384320:2)
    script.js?r=1568234963-38:254 LiveChatWidget.getUserData: method can be called after fired event — onBitrixLiveChat
    t @ script.js?r=1568234963-38:254
    script.js?r=1568234963-38:254 LiveChatWidget.getUserData: method can be called after fired event — onBitrixLiveChat
    t @ script.js?r=1568234963-38:254

    сам сайт tesla14.ru. так же вопрос как добавить кнопку чтобы можно было убрать товар и изменять количество

    да использую тот же шаблон

    • Ошибка никак не связана с компонентом

      • Афанасий:

        ага не с компонентом. Разобрался, еще такой вопрос. если я оформляю не под авторизованным пользователем то выходит ошибка
        Mysql query error: (1062) Duplicate entry ‘0-1’ for key ‘IX_SOPV_ORD_PROP_UNI’
        если авторизован то все нормально. в бд создаются поля с order-id 0 удаляю их по новой заказ делаю та же самая история

        • Афанасий:

          есть какие-нибудь идеи как решить эту проблему? с обычным модулем нормально работает можно оформить не авторизованным человеком

  • Афанасий:

    а где его можно найти, ненашел

  • Дмитрий:

    Пробовал подгрузить данные через ajax вывел нужные поля но при проверке полей на стадии сохранения заказа функцией saveOrderAction в $validationResult = $componentClass->validateOrder(); выскакивает ошибка о том что новые поля которые я добавил не заполнены, хотя выше по коду эти поля есть, значения полей заполнены и добавляются в заказ $componentClass->setOrderProperties($properties); В чем может быть проблема?

    • Что именно у тебя в $properties?
      Массив типа «код свойства» — «значение свойства»? Или там в массиве в качестве ключей ID свойств?
      В админке есть свойства с пустыми кодами? Есть свойства у которых одинаковые коды (вообще, не в массиве а в списке свойств в админке)?

  • Дмитрий:

    Данные получаю так:
    public function getPropsAndDeliveryAction(int $person_type_id, array $properties, int $delivery_id): array
    {
    CBitrixComponent::includeComponentClass(‘opensource:order’);
    $componentClass = new OpenSourceOrderComponent();
    $componentClass->createVirtualOrder($person_type_id);
    $componentClass->setOrderProperties($properties);
    $componentClass->createOrderShipment($delivery_id);
    $componentClass->createOrderPayment(1);
    $data=[];

    $paymentCollection = $componentClass->order->getPaymentCollection();
    $payment = $componentClass->order->getPaymentCollection()->current();
    $availablePaySystems = PaySystem\Manager::getListWithRestrictions($payment);
    $data[‘paysystem’] = $availablePaySystems;

    $propertyCollection = $componentClass->order->getPropertyCollection();
    foreach ($propertyCollection as $prop) {
    $data[‘properties’][$prop->getField(‘CODE’)] = $prop->getField(‘NAME’);
    }
    $data[‘sumTotal’][‘orderPrice’] = SaleFormatCurrency($componentClass->order->getPrice(), $componentClass->order->getCurrency());
    $data[‘sumTotal’][‘orderDeliveryPrice’] = SaleFormatCurrency($componentClass->order->getDeliveryPrice(), $componentClass->order->getCurrency());
    return $data;
    }
    Соответственно после получения данных размещаю их на странице и отправляю после заполнения ajax-ом:

    public function saveOrderAction(int $person_type_id, array $properties, int $delivery_id, int $pay_system_id): array
    {
    $data = [];

    CBitrixComponent::includeComponentClass(‘opensource:order’);

    $componentClass = new OpenSourceOrderComponent();
    $componentClass->createVirtualOrder($person_type_id);
    $componentClass->setOrderProperties($properties);
    $componentClass->createOrderShipment($delivery_id);
    $componentClass->createOrderPayment($pay_system_id);

    $validationResult = $componentClass->validateOrder();
    if ($validationResult->isSuccess()) {
    $saveResult = $componentClass->order->save();
    if ($saveResult->isSuccess()) {
    $data[‘saved’] = true;
    $data[‘order_id’] = $saveResult->getId();
    } else {
    $this->errorCollection->add($saveResult->getErrors());
    }
    } else {
    $this->errorCollection->add($validationResult->getErrors());
    }

    return $data;
    }

  • Дмитрий:

    Перепроверил со стандартным шаблоном компонента(без правок), такая же ерунда, дело не в ajax-е видимо после обновления битрикс все сломалось.

    • Хз про что ты, но на всякий случай проверил работу компонента с последней версией битрикса. Прям вообще всё что есть установил до 19 версии включительно. Поставил из композера компонент. Заказ корректно со всеми свойствами сохранился. На демо шаблоне с доставкой всё тоже нормально пересчитывается и сохраняется

      • Дмитрий:

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

        • Ок 🙂
          Свойства привязанные к доставке я в целом тестировал, работает нормально. Но если по итогу будет что добавить в код компонента — кидай merge request в репозиторий. Я твой код в основной волью.

          • Дмитрий:

            Ок. Приведу код в порядок, добавлю на git.

  • Виталий:

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

    • Виталий:

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

  • Виталий:

    Проблема тоже решена)

  • Андрей:

    Огромное спасибо Автору!!!

    Подскажите пожалуйста как в ajax.php собрать метод который сформирует мне массив цен: Цена всех товаров, цена доставки, суммарная цена товары + доставка

    Спасибо!

    • Андрей:

      И еще хотелось бы понять как по аналогии с доступными методами доставки для выбранного региона получать доступные методы оплаты.

      Спасибо!

  • Андрей:

    Подскажите пожалуйста, почему-то не работает Sejectize: http://skrinshoter.ru/s/301219/jmz9meCZ и соответсвенно невозможно указать местоположение

  • Александр:

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

    • Установить можно только модуль, а не компонент. В админке, в списке модулей маркетплейс есть модуль? Статус у него установлен? Если установлен, то внимательнее список компонентов посмотри.

      Кеш всего сайта сбрось попробуй.

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

  • Айдар:

    Просто большое человеческое спасибо!!!

  • Про:

    Спасибо за компонент!
    Подскажите, должно ли работать изменение стоимости доставки на странице заказа при выборе службы доставки?
    Или это надо реализовывать через calculateDeliveriesAction?

    • Должно работать где? В каком то из демо шаблонов?
      В демо шаблонах я делал пересчет стоимости доставки при смене местоположения. Итоговую сумму в демо шаблоне я вроде не пересчитывал.

      • Про:

        Разместил компонент (шаблон default) на странице заказа. В настройках: Служба доставки по умолчанию — Курьером. При перезагрузке страницы в списке служб доставки «Курьером» не выделяется, но стоимость доставки есть. При выборе другой доставки стоимость не пересчитывается. Я так понял, это нормально? Как сделать пересчет стоимости, через ajax запрос?

    • Про:

      Разобрался. Вопрос снят.

  • Василий:

    Добрый день! Вы написали, что в событии перед сохранением можно добавлять ошибки в уже общую коллекцию ошибок, чтобы в шаблоне выводить их:
    «В OnSaleOrderBeforeSaved можно добавить любые проверки по данным заказа и возвращать ошибки. Эти ошибки будут добавлены в общую коллекцию ошибок и переданы в шаблон.»

    Подскажите пожалуйста, каким образом добавлять в коллекцию? Выбрасывать и Большое спасибо!

  • Василий:

    Добрый день! Не совсем понял отправил ли я сообщение или нет, продублирую.
    Александр, вы написали что в событии OnSaleOrderBeforeSaved можно делать валидацию данных и ошибки добавлять в коллекцию ошибок. Подскажите, каким образом можно добавить ошибку в коллекцию из функции, которое выполняется по событию? Очень нужна ваша помощь, Спасибо!

    • Пока что нет времени проверить. Попробуй просто исключение выбросить в обработчике. Если не сработает, то возвращай EventResult из функции обработчика
      https://dev.1c-bitrix.ru/api_d7/bitrix/main/entity/eventresult/index.php

      Что то вроде такого должно получиться:

      static function onSaleOrderBeforeSaved(\Bitrix\Main\Event $event)
      {
      	return new \Bitrix\Main\EventResult(
      		\Bitrix\Main\EventResult::ERROR,
      		new \Bitrix\Sale\ResultError('Ошибка'),
      		'sale'
      	);
      }

      код писал прямо здесь. Поэтому не факт что он заработает именно в таком виде.

      • Василий:

        Большое спасибо, с помощью EventResult возвращает все отлично!
        Исключения тоже пробовал, только почему-то при одном варианте оплаты всё отлично, при другом исключение останавливает работу сайта.

  • Добрый день! Подскажите какие образом сохранять свойства типа файл? При стандартном input type=file name=properties[CODE] Выдает ошибку — Свойство заказа «Имя свойства» — ошибка ввода

  • Александр:

    Очень крутой и детальный разбор. Спасибо, коллега!

  • Игорь:

    Здравствуйте. Спасибо за ваш труд, очень познавательно. Подскажите, а как вы решали проблему с расчетом доставки, когда службе доставки для расчета требуется дополнительная информация для расчета доставки, когда не достаточно просто радио-баттонами выбирать ID доставки что бы посчитать ее стоимость.
    Например у службы доставки Boxberry, в стандартной корзине, при выборе профиля доставки «Доставка до пукнта выдачи (ПВЗ)», появляется ссылка с предложением выбрать на карте пункт выдачи заказа. По ссылке открывается виджет из компонета модуля Boxberry и только после выбора ПВЗ в окне виджета считаетмся стомость доставки.
    Аналогичным образом работает и СДЭК и и наверняка кто-то еще.
    Спасибо.

  • Алина:

    Получаю ошибку Using $this when not in object context на 16 строке $component = $this->__component. Не пойму как исправить, помогите пожалуйста

  • xxxx:

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

Добавить комментарий для xxxx Отменить ответ

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