Всего 125 668 комментариев

Михаил
Вчера в 03:33
0
Вот так отображаются поля довольно красиво! Нужно создать поля: allowed_resources, date_start, date_end в базе данных в таблице modx_user_attributes. Это плагин:
<?php
/**
 * Plugin for adding custom fields to user profile
 * Events: OnMODXInit, OnUserFormPrerender, OnUserFormSave
 */

switch ($modx->event->name) {
    case "OnMODXInit":
        // Добавляем поля в карту объектов MODX
        $map = array(
            'modUserProfile' => array(
                'fields' => array(
                    'allowed_resources' => null,
                    'date_start' => null,
                    'date_end' => null,
                ),
                'fieldMeta' => array(
                    'allowed_resources' => array(
                        'dbtype' => 'varchar',
                        'precision' => '191',
                        'phptype' => 'string',
                        'null' => true,
                        'default' => null,
                    ),
                    'date_start' => array(
                        'dbtype' => 'int',
                        'precision' => '20',
                        'phptype' => 'integer',
                        'null' => true,
                        'default' => null,
                    ),
                    'date_end' => array(
                        'dbtype' => 'int',
                        'precision' => '20',
                        'phptype' => 'integer',
                        'null' => true,
                        'default' => null,
                    ),
                ),
            ),
        );

        foreach ($map as $class => $data) {
            $modx->loadClass($class);
            foreach ($data['fields'] as $field => $default) {
                if (!isset($modx->map[$class]['fields'][$field])) {
                    $modx->map[$class]['fields'][$field] = $default;
                    if (isset($data['fieldMeta'][$field])) {
                        $modx->map[$class]['fieldMeta'][$field] = $data['fieldMeta'][$field];
                    }
                }
            }
        }
        break;
    
    case "OnUserFormPrerender":
        // Проверяем существование пользователя
        if (!isset($user) || !is_object($user) || $user->get('id') < 1) {
            return;
        }

        $profile = $user->getOne('Profile');
        if (!$profile) {
            return;
        }

        // Получаем значения полей
        $allowedResources = $profile->get('allowed_resources') ?: '';
        $dateStart = $profile->get('date_start');
        $dateEnd = $profile->get('date_end');

        // Форматируем даты
        $dateStartFormatted = (!empty($dateStart) && is_numeric($dateStart)) 
            ? date('Y-m-d', $dateStart) 
            : '';
        $dateEndFormatted = (!empty($dateEnd) && is_numeric($dateEnd)) 
            ? date('Y-m-d', $dateEnd) 
            : '';

        // Экранируем для JavaScript
        $allowedResources = htmlspecialchars($allowedResources, ENT_QUOTES, 'UTF-8');

        $js = <<<JS
<script type="text/javascript">
Ext.onReady(function() {
    Ext.ComponentMgr.onAvailable('modx-user-tabs', function(panel) {
        panel.on('beforerender', function() {
            // Находим левую колонку первой вкладки
            var tabPanel = this.items && this.items.items && this.items.items[0];
            if (!tabPanel) return;
            
            var formPanel = tabPanel.items && tabPanel.items.items && tabPanel.items.items[0];
            if (!formPanel) return;
            
            var leftCol = formPanel.items && formPanel.items.items && formPanel.items.items[0];
            if (!leftCol) return;

            // Создаем FieldSet с правильным layout
            var customFields = new Ext.form.FieldSet({
                title: 'Доступ к ресурсам',
                collapsible: true,
                collapsed: false,
                style: 'margin-top: 15px; margin-bottom: 15px;',
                autoHeight: true,
                layout: 'form',  // КЛЮЧЕВОЕ: включает отображение fieldLabel
                labelWidth: 150,
                anchor: '100%',
                defaults: {
                    anchor: '100%',
                    msgTarget: 'under'
                },
                items: [
                    {
                        xtype: 'textfield',
                        name: 'allowed_resources',
                        fieldLabel: 'Разрешенные ресурсы',
                        maxLength: 191,
                        value: '{$allowedResources}',
                        listeners: {
                            afterrender: function(cmp) {
                                if (Ext.QuickTips) {
                                    Ext.QuickTips.register({
                                        target: cmp.getEl(),
                                        text: 'ID ресурсов через запятую (например: 1,5,12,43)'
                                    });
                                }
                            }
                        }
                    },
                    {
                        xtype: 'datefield',
                        name: 'date_start',
                        fieldLabel: 'Дата начала доступа',
                        format: 'Y-m-d',
                        value: '{$dateStartFormatted}',
                        emptyText: 'Выберите дату начала'
                    },
                    {
                        xtype: 'datefield',
                        name: 'date_end',
                        fieldLabel: 'Дата окончания доступа',
                        format: 'Y-m-d',
                        value: '{$dateEndFormatted}',
                        emptyText: 'Выберите дату окончания'
                    }
                ]
            });

            // Добавляем в левую колонку
            leftCol.add(customFields);
            
            // Обновляем layout
            if (leftCol.doLayout) {
                leftCol.doLayout();
            }
            if (panel.doLayout) {
                panel.doLayout();
            }
        });
    });
});
</script>

<style type="text/css">
/* Стили для полей */
.x-fieldset {
    border: 1px solid #d0d0d0;
    padding: 10px;
    margin-bottom: 10px;
}
.x-fieldset legend {
    font-weight: bold;
    color: #333;
    padding: 0 5px;
}
.x-form-item {
    margin-bottom: 10px;
}
.x-form-item-label {
    font-weight: bold !important;
    color: #555;
    font-size: 12px;
}
.x-form-field-wrap {
    margin-bottom: 5px;
}
</style>
JS;

        $modx->controller->addHtml($js);
        break;
        
    case "OnUserFormSave":
        if (!isset($user) || !is_object($user)) {
            return;
        }
        
        $profile = $user->getOne('Profile');
        if (!$profile) {
            return;
        }
        
        $modified = false;
        
        // Сохраняем allowed_resources
        if (isset($_POST['allowed_resources'])) {
            $value = trim($_POST['allowed_resources']);
            $profile->set('allowed_resources', $value !== '' ? $value : null);
            $modified = true;
        }
        
        // Сохраняем date_start
        if (isset($_POST['date_start'])) {
            if (!empty($_POST['date_start'])) {
                $timestamp = strtotime($_POST['date_start']);
                if ($timestamp !== false) {
                    $profile->set('date_start', $timestamp);
                    $modified = true;
                }
            } else {
                $profile->set('date_start', null);
                $modified = true;
            }
        }
        
        // Сохраняем date_end
        if (isset($_POST['date_end'])) {
            if (!empty($_POST['date_end'])) {
                $timestamp = strtotime($_POST['date_end']);
                if ($timestamp !== false) {
                    $profile->set('date_end', $timestamp);
                    $modified = true;
                }
            } else {
                $profile->set('date_end', null);
                $modified = true;
            }
        }
        
        // Сохраняем профиль только если были изменения
        if ($modified) {
            $profile->save();
        }
        break;
}
Александр Туниеков
22 февраля 2026, 19:58
0
Кто реально внедрил MODX с Vue. js — каков опыт?
Опыт — реактивные переменные благословение и проклятие Vue. С ними можно делать почти мнгновенно реагирующий интерфейс и неприятность что в них не всегда оказывается то что нужно. Где то в коде именил или не изменил не так как нужно и приложение ведет себя не так как нужно. Отследить изменения тяжело особенно если меняешь чужой код или код ИИ
Александр Туниеков
22 февраля 2026, 19:33
+1
Я делаю miniERP систему на модекс. ИИ помогает значительно. Но с ИИ лучше делать когда сам понимаешь как сделать. Нейросеть не догоняет весь контекст. И сочиняет на ходу чтобы как-бы выполнить задачу. Правда у меня нейросеть хотя бы пытается что-то сделать, а люди сразу в ступор впадают :-). Непривычный контекст задач и тяжело въехать.
Я делаю систему с нейросетью используя modExtra и PVExtra, так как IDE заточены под файлы и в базу не лезут. А в MODX пользовательский код обычно в базе. То есть ставлю чистый MODX. Делаю на нем ЧПУ и делаю компоненты которые заполняют сайт. Возможно имеет смысл подключить к нейросети MCP MySQL и чтоб нейросеть напрямую редактировала страницы и шаблоны. Чанки, сниппеты и тв в последнее время не использую. Вместо сниппетов vue компоненты к которым чанки не особо прикрутишь. А вместо тв проще нужные таблицы сразу писать.
Мне проше сразу писать компоненты. Некоторые потом можно отдельно выкладывать. И проще в прод вносить изменения. Просто поставил компонент и нужные правки применились. И не надо делать деплой всего сервера. Правки по компонентам и нужно поставить лишь тот компонент в котором изменения.

С нейросеть у меня 2 метода писать код:
1. Точечное промт-програмирование. Например, пишешь промпт: здесь отсортируй массив по возрастанию ранга наряда. И нейросеть вставляет нужный код.
2. Вайб-кодинг. Есть задача которую непонятно как сделать и пишешь, например, нужно сделать дополнительные фильтры чекбоксы которые выводят список значений в таблице и при выборе которых в таблице фильтруется по этим выбранным значениям. И нейросеть сама разбирается что сделать и пишет код. Такой промпт я как-то задавал и потом 3 часа разбирался почему основные фильтры не работают. Оказалось что нейросеть решила, что если есть допфильтры, то основные не нужны :-).

3 месяц писал с Kilo Code для VSCode. Но постепенно работа через API Claude стала забирать все больше денег. Сейчас использую Claude Code для VSCode с подпиской MAX. Она втрое дешевле должна выходить. Вчера сочинял с ним план внедрения в miniERP продажи перепродоваемых товаров (вместе с товарами которые мы изготавливаем). Перед использованием вайб-кодинга лучше с ИИ сочинить план разработки.

Ну вкратце так. Более подробно пока желания писать нет. Хотя есть моменты которые хочется обсудить или написать статьи, но прям сейчас только то что написал.
W.H.I.T.E
22 февраля 2026, 18:33
0
Когда примерно ожидать версию под MODX3?
Тодор
22 февраля 2026, 17:10
0
Ето ты сделал?
public function getReceiverId() {
  error_log("🔥🔥🔥 getCustomerId() НАЧАЛО 🔥🔥🔥");
  $customer = null;
  $response = $this->ms2->invokeEvent('msOnBeforeGetOrderCustomer', [//вместо $this->
    'order' => $this,//Вместо $this->order,
    'customer' => $customer,
  ]);
  if (!$response['success']) {
    return $response['message'];
  }

  if (!$customer) {
    $data = $this->get();//Вместо $this->order->get();
    
    ...
      $this->set($data);//Вмесо $this->order->set($data);
    ...
  }

  $response = $this->ms2->invokeEvent('msOnGetOrderCustomer', [//Вместо $this->
    'order' => $this, // Вместо $this->order,
    'customer' => $customer,
  ]);
  if (!$response['success']) {
    return $response['message'];
  }

  return $customer instanceof modUser ? $customer->get('id') : 0;
}
возможно что-то упустил
Дмитрий
22 февраля 2026, 16:46
0
ух, спасибо за разъяснения. Т.е. фактически понятие «регистрация службы» это просто изменение системных настроек?

с разным расположением файла в зависимости от версии, я уже споткнулся на этом моменте когда подключал свою службу доставки, кстати, в мануале, как раз таки не правильно указано расположения файла (по всей видимости для старой версии)
Я разобрался почему у меня не подключался мой класс, в системных настройках в значении переменной ms2_order_handler_class, у меня было указано «my_OrderHandler » с пробелом на конце, очень было обидно осознать это, когда я два дня пытался разобраться, чой-то мой класс не подключается.

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

Я начал с того что поменял исходники, и собственно, все у меня работало так как надо.
Телефон приводился к нужному формату, вместо емейла записывался номер телефона, и вообще жизнь прекрасна и удивительна, но!
Когда, все же заработал мой класс, и я сделал, как Вы советовали:
я скопировал метод submit, в нем изменил $user_id = $this->ms2->getCustomerId() на $user_id = $this->getReceiverId();

и новый метод, поменял у него название на getReceiverId()…
тут все работать перестало.

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

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

Помогите, я снова в тупике.
Тодор
21 февраля 2026, 23:53
0
Етот кусок кода отвечает за то что бы менялась настройка ms2_services
<?php
if ($miniShop2 = $modx->getService('miniShop2')) {
  $miniShop2->addService('order', 'my_OrderHandler',
      '{core_path}components/minishop2/custom/order/my_msorderhandler.class.php');
}
После чего у тебя ms2_services должен быть таким
{"cart":[],"order":{"my_OrderHandler":"{core_path}components\/minishop2\/custom\/order\/my_msorderhandler.class.php"},"payment":[],"delivery":{"mydelivery":"{core_path}components\/minishop2\/custom\/delivery\/my_msdeliveryhandler.class.php"}}
Тебе надо делать только одно из двух либо регистировать клас через консоль либо в сис. настройках.

В зависимости от версии minishopа может быть разный путь к msOrderHandler, убедись что етот файл существует
require_once MODX_CORE_PATH . 'components/minishop2/handlers/msorderhandler.class.php';
А еще в submit ты поменял $user_id = $this->ms2->getCustomerId(); на $user_id = $this->getReceiverId(); Но новый метод ты не назвал getReceiverId
Futuris
21 февраля 2026, 10:34
+1
В какое славное время я взялся за изучение кода)) Только выучил элементарную базу PHP, как выясняется, что это скоро и не нужно никому будет). Расходимся! Правда пока, по опыту общения с нейронками я вижу, что даже элементарные знания помогают и сильно сокращают время. А там, где полагаешься только на робота, авось как-то вывезет сам — там тратится неимоверное количество итераций, чтобы получить какой-то рабочий результат.
SYAN
21 февраля 2026, 01:40
0
Далее код оставил без изменений
<script type="text/javascript">
    // <![CDATA[
    {literal}
    Ext.onReady(function () {
        var fld = MODx.load({
            {/literal}
            xtype: 'textfield',
            applyTo: 'tv{$tv->id}',
            width: '99%',
            id: 'tv{$tv->id}',
            enableKeyEvents: true,
            allowBlank: {if $params.allowBlank == 1 || $params.allowBlank == 'true'}true{else}false{/if},
            value: '{$tv->value}',
            {literal}
            listeners: {'change': {fn: MODx.fireResourceFormChange, scope: this}},
        });
        Ext.getCmp('modx-panel-resource').getForm().add(fld);
        MODx.makeDroppable(fld);
    });
    {/literal}
    // ]]>
</script>

{$fields = [
'title' => $_config.videogallery_field_title,
'desc' => $_config.videogallery_field_desc,
'image' => $_config.videogallery_field_image,
'video' => $_config.videogallery_field_video,
'videoId' => $_config.videogallery_field_videoId,
'videoDuration' => $_config.videogallery_field_videoDuration
]}
{foreach from=$fields key=name item=item}
    {if $item != ''}
        {if substr($item, 0,3) == 'tv.'}
            {$vg_fields[$name] = str_replace('tv.', '', $item)}
            {$input_ids_data = "tv`$modx->getObject('modTemplateVar', ['name'=>$vg_fields[$name]] )->id`"}

            {if $input_ids_data != 'tv' AND $input_ids_data != ''}
                {$input_ids[$name] = $input_ids_data}
            {/if}
        {else}
            {$vg_fields[$name] = $item}
            {$input_ids[$name] = "modx-resource-`$vg_fields[$name]`"}
        {/if}
    {/if}
{/foreach}

<script type="text/javascript">
    // <![CDATA[
    Ext.onReady(function () {
        if (typeof vgHandlers == 'undefined') {
            vgHandlers = {
                /* */
            };
        }
        vgHandlers['{$tv->id}'] = new videoGalleryHandler({
            tv: '{$tv->id}',
            tvid: '{$tvid}',
            resource: {$modx->resource->id},
            actionUrl: MODx.config.assets_url + 'components/videogallery/connector.php?HTTP_MODAUTH=' + MODx.siteId + '&wctx=mgr&action=mgr/gallery/handle',
            selectors: {
                tvInput: '#tv{$tv->id}',
            },
            callbacks: {
                success: function (resp) {
                    var element = {
                        /* */
                    };
                    element['{$tv->id}'] = {
                        /* */
                    };

                    {foreach from=$fields key=name item=field}
                    {if $field != '' AND $field != 'tv'}
                    if (resp.object.hasOwnProperty('{$name}')) {
                        element['{$tv->id}']['{$name}'] = document.querySelector('#{$input_ids[$name]}');
                        if (element['{$tv->id}']['{$name}']) {
                            element['{$tv->id}']['{$name}'].value = resp.object['{$name}'];
                        }
                    }
                    {/if}
                    {/foreach}
                },
                reset: function () {
                    var element = {
                        /* */
                    };
                    element['{$tv->id}'] = {
                        /* */
                    };

                    {foreach from=$fields key=name item=field}
                    {if $field != '' AND $field != 'tv'}
                    element['{$tv->id}']['{$name}'] = document.querySelector('#{$input_ids[$name]}');
                    if (element['{$tv->id}']['{$name}']) {
                        element['{$tv->id}']['{$name}'].value = '';
                    }
                    {/if}
                    {/foreach}
                },
            },
        });
    });
    // ]]>
</script>

{* Fix "Smarty Compiler: Syntax error {/foreach} unclosed {foreach} tag" *}
{foreach from=$fields key=name item=field}
{/foreach}
SYAN
21 февраля 2026, 01:38
0
Функция сохранения
<script type="text/javascript">
// <![CDATA[
Ext.onReady(function () {
    // Сохраняем исходные значения при загрузке
    const originalValues = {
        title: document.getElementById('vgTitle_{$tv->id}').value,
        desc: document.getElementById('vgDesc_{$tv->id}').value,
        video: document.getElementById('vgVideo_{$tv->id}').value,
        videoId: document.getElementById('vgVideoId_{$tv->id}').value,
        image: document.getElementById('vgImage_{$tv->id}').value,
        videoDuration: document.getElementById('vgVideoDuration_{$tv->id}').value
    };

    window.compareAndSave = function(tvId) {
        const currentValues = {
            title: document.getElementById('vgTitle_' + tvId).value,
            desc: document.getElementById('vgDesc_' + tvId).value,
            video: document.getElementById('vgVideo_' + tvId).value,
            videoId: document.getElementById('vgVideoId_' + tvId).value,
            image: document.getElementById('vgImage_' + tvId).value,
            videoDuration: document.getElementById('vgVideoDuration_' + tvId).value
        };

        // Проверяем, есть ли изменения
        let hasChanges = false;
        for (const key in currentValues) {
            if (currentValues[key] !== originalValues[key]) {
                hasChanges = true;
                break;
            }
        }

        if (!hasChanges) {
            alert('Нет изменений для сохранения.');
            return;
        }

        // Формируем новый JSON
        const newJson = JSON.stringify(currentValues);

        // Обновляем скрытое поле
        document.getElementById('tv' + tvId).value = newJson;

        // Сигнализируем MODX об изменении
        if (typeof MODx !== 'undefined' && MODx.fireResourceFormChange) {
            MODx.fireResourceFormChange();
        }

        alert('Данные сохранены!');

        // Обновляем исходные значения (чтобы следующие изменения сравнивались корректно)
        Object.assign(originalValues, currentValues);
    };

    // Интеграция с videoGalleryHandler
    if (typeof vgHandlers !== 'undefined' && vgHandlers['{$tv->id}']) {
        vgHandlers['{$tv->id}'].callbacks.success = function(resp) {
            // Обновляем поля формы
            document.getElementById('vgVideo_{$tv->id}').value = resp.object.video || '';
            document.getElementById('vgVideoId_{$tv->id}').value = resp.object.videoId || '';
            document.getElementById('vgImage_{$tv->id}').value = resp.object.image || '';
        };
    }
});
// ]]>
</script>

<style>
.videogallery-wrapper { padding:10px; border:1px solid #ddd; border-radius:4px; }
.videogallery-fields div { margin:10px 0; }
label { display:inline-block; width:120px; font-weight:bold; }
input,textarea { width:100%; padding:5px; box-sizing:border-box; }
</style>
Олег Захаров
21 февраля 2026, 01:17
0
По этому вопросу тоже думаю — создал вопрос тут
Николай Савин
20 февраля 2026, 20:13
0
Не нужно меня поддерживать в данном конкретном случае. Прошу убрать лишнее
Дмитрий
20 февраля 2026, 19:28
0
В общем убил целый день, но… так у меня ничего и не вышло.
не могу зарегистрировать класс.

содержание самого файла my_msorderhandler.class.php:

<?php
if (!class_exists('msOrderHandler')) {  // проверяем интерфейс заказа
    require_once MODX_CORE_PATH . 'components/minishop2/handlers/msorderhandler.class.php';
}

class my_OrderHandler extends msOrderHandler {  // implements msOrderInterface не нужен
    public function submit($data = [])
        {
            
            $response = $this->ms2->invokeEvent('msOnSubmitOrder', [
                'data' => $data,
                'order' => $this,
            ]);
            if (!$response['success']) {
                return $this->error($response['message']);
            }
            if (!empty($response['data']['data'])) {
                $this->set($response['data']['data']);
            }
    
            $response = $this->getDeliveryRequiresFields();
            if ($this->ms2->config['json_response']) {
                $response = json_decode($response, true);
            }
            if (!$response['success']) {
                return $this->error($response['message']);
            }
            $requires = $response['data']['requires'];
    
            $errors = [];
            foreach ($requires as $v) {
                if (!empty($v) && empty($this->order[$v])) {
                    $errors[] = $v;
                }
            }
            if (!empty($errors)) {
                return $this->error('ms2_order_err_requires', $errors);
            }
    
            $user_id = $this->getReceiverId();
            if (empty($user_id) || !is_int($user_id)) {
                return $this->error(is_string($user_id) ? $user_id : 'ms2_err_user_nf');
            }
    
            $cart_status = $this->ms2->cart->status();
            if (empty($cart_status['total_count'])) {
                return $this->error('ms2_order_err_empty');
            }
    
            $delivery_cost = $this->getCost(false, true);
            $cart_cost = $this->getCost(true, true) - $delivery_cost;
            $num = $this->getNewOrderNum();
    
            /** @var msOrder $msOrder */
            $msOrder = $this->storageHandler->getForSubmit(
                compact('user_id', 'num', 'cart_cost', 'cart_status', 'delivery_cost')
            );
    
            $response = $this->ms2->invokeEvent('msOnBeforeCreateOrder', [
                'msOrder' => $msOrder,
                'order' => $this,
            ]);
            if (!$response['success']) {
                return $this->error($response['message']);
            }
    
            if ($msOrder->save()) {
                $response = $this->ms2->invokeEvent('msOnCreateOrder', [
                    'msOrder' => $msOrder,
                    'order' => $this,
                ]);
                if (!$response['success']) {
                    return $this->error($response['message']);
                }
    
                if ($this->storage === 'session') {
                    $this->ms2->cart->clean();
                    $this->clean();
                }
                if (empty($_SESSION['minishop2']['orders'])) {
                    $_SESSION['minishop2']['orders'] = [];
                }
                $_SESSION['minishop2']['orders'][] = $msOrder->get('id');
    
                // Trying to set status "new"
                $status_new = $this->modx->getOption('ms2_status_new', null, 1);
                $response = $this->ms2->changeOrderStatus($msOrder->get('id'), $status_new);
                if ($response !== true) {
                    return $this->error($response, ['msorder' => $msOrder->get('id')]);
                }
    
                // Reload order object after changes in changeOrderStatus method
                /** @var msOrder $msOrder */
                $msOrder = $this->modx->getObject('msOrder', ['id' => $msOrder->get('id')]);
    
                /** @var msPayment $payment */
                $payment = $this->modx->getObject(
                    'msPayment',
                    ['id' => $msOrder->get('payment'), 'active' => 1]
                );
                if ($payment) {
                    $response = $payment->send($msOrder);
                    if ($this->config['json_response']) {
                        @session_write_close();
                        echo is_array($response) ? json_encode($response) : $response;
                        die();
                    }
                    if (!empty($response['data']['redirect'])) {
                        $this->modx->sendRedirect($response['data']['redirect']);
                    }
                    if (!empty($response['data']['msorder'])) {
                        $redirect = $this->modx->context->makeUrl(
                            $this->modx->resource->id,
                            ['msorder' => $response['data']['msorder']]
                        );
                        $this->modx->sendRedirect($redirect);
                    }
                    $this->modx->sendRedirect($this->modx->context->makeUrl($this->modx->resource->id));
                } else {
                    if ($this->config['json_response']) {
                        return $this->success('', ['msorder' => $msOrder->get('id')]);
                    }
                    $redirect = $this->modx->context->makeUrl(
                        $this->modx->resource->id,
                        ['msorder' => $msOrder->get('id')]
                    );
                    $this->modx->sendRedirect($redirect);
                }
                return $this->success();
            }
    
            return $this->error();
        }
        
        
        public function getCustomerId() {
    error_log("🔥🔥🔥 getCustomerId() НАЧАЛО 🔥🔥🔥");

        $customer = null;

        $response = $this->invokeEvent('msOnBeforeGetOrderCustomer', [
            'order' => $this->order,
            'customer' => $customer,
        ]);
        if (!$response['success']) {
            return $response['message'];
        }

        if (!$customer) {
            $data = $this->order->get();
            $email = $data['email'] ?? '';
            $receiver = $data['receiver'] ?? '';
            $phone = $data['phone'] ?? '';
            
            // Функция для приведения телефона к международному формату +КодСтраныНомер
            $formatPhone = function($phone) {
                if (empty($phone)) return '';
                
                // Удаляем все нецифровые символы
                $phone = preg_replace('/[^0-9]/', '', $phone);
                
                // Определяем код страны по умолчанию (Россия)
                $defaultCountryCode = '7';
                
                // Если номер начинается с 8 (Россия)
                if (substr($phone, 0, 1) == '8') {
                    $phone = $defaultCountryCode . substr($phone, 1);
                }
                // Если номер без кода (10 цифр) - добавляем код по умолчанию
                elseif (strlen($phone) == 10) {
                    $phone = $defaultCountryCode . $phone;
                }
                // Если номер начинается с 7 (уже есть код России)
                elseif (substr($phone, 0, 1) == '7' && strlen($phone) == 11) {
                    // Оставляем как есть
                }
                // Если номер с другим кодом страны
                elseif (strlen($phone) > 11 || (strlen($phone) == 11 && substr($phone, 0, 1) != '7')) {
                    // Оставляем как есть (уже содержит код страны)
                }
                
                // Добавляем знак + в начало
                return '+' . $phone;
            };
            
            // Приводим телефон к единому формату
            $formattedPhone = $formatPhone($phone);
            
            // Обновляем телефон в данных заказа
            if ($formattedPhone !== $phone) {
                $data['phone'] = $formattedPhone;
                $this->order->set($data);
                $phone = $formattedPhone;
            }
            
            // Формируем receiver, если он пустой
            if (empty($receiver)) {
                if (!empty($phone)) {
                    $receiver = preg_replace('/[^0-9]/', '', $phone);
                } elseif (!empty($email)) {
                    $receiver = substr($email, 0, strpos($email, '@'));
                } else {
                    $receiver = uniqid('user_', false);
                }
            }
            
            // Формируем email, если он пустой
            if (empty($email)) {
                if (!empty($phone)) {
                    $cleanPhone = preg_replace('/[^0-9]/', '', $phone);
                    $email = 'user_' . $cleanPhone . '@' . $this->modx->getOption('http_host');
                } else {
                    $email = $receiver . '@' . $this->modx->getOption('http_host');
                }
            }

            // Если пользователь авторизован
            if ($this->modx->user->isAuthenticated()) {
                $profile = $this->modx->user->Profile;
                
                if (!$profile->get('email')) {
                    $profile->set('email', $email);
                }
                
                if (!empty($phone) && $profile->get('mobilephone') != $phone) {
                    $profile->set('mobilephone', $phone);
                }
                
                if (!empty($receiver) && $profile->get('fullname') != $receiver) {
                    $profile->set('fullname', $receiver);
                }
                
                $profile->save();
                $customer = $this->modx->user;
                
            } else {
                // Поиск существующего пользователя
                $c = $this->modx->newQuery('modUser');
                $c->leftJoin('modUserProfile', 'Profile');
                
                $filter = [];
                
                if (!empty($phone)) {
                    $filter['modUser.username'] = $phone;
                    $filter['OR:Profile.mobilephone:='] = $phone;
                }
                
                if (!empty($email)) {
                    $filter['OR:Profile.email:='] = $email;
                }
                
                $c->where($filter);
                $c->select('modUser.id');
                
                // Пытаемся найти пользователя
                if (!$customer = $this->modx->getObject('modUser', $c)) {
                    // Создаем нового пользователя
                    $userData = [
                        'username' => !empty($phone) ? $phone : $email,
                        'password' => md5(rand()),
                        'active' => 1
                    ];
                    
                    $customer = $this->modx->newObject('modUser', $userData);
                    
                    $profileData = [
                        'fullname' => $receiver,
                    ];
                    
                    if (!empty($email)) {
                        $profileData['email'] = $email;
                    }
                    
                    if (!empty($phone)) {
                        $profileData['mobilephone'] = $phone;
                    }
                    
                    $profile = $this->modx->newObject('modUserProfile', $profileData);
                    $customer->addOne($profile);
                    
                    // Добавляем настройку языка
                    $setting = $this->modx->newObject('modUserSetting');
                    $setting->fromArray([
                        'key' => 'cultureKey',
                        'value' => $this->modx->getOption('cultureKey', null, 'en', true),
                    ], '', true);
                    $customer->addMany($setting);
                    
                    if (!$customer->save()) {
                        $customer = null;
                    } elseif ($groups = $this->modx->getOption('ms2_order_user_groups', null, false)) {
                        $groupRoles = array_map('trim', explode(',', $groups));
                        foreach ($groupRoles as $groupRole) {
                            $groupRole = explode(':', $groupRole);
                            $roleId = null;
                            if (count($groupRole) > 1 && !empty($groupRole[1])) {
                                $roleId = is_numeric($groupRole[1]) ? (int)$groupRole[1] : $groupRole[1];
                            }
                            $customer->joinGroup($groupRole[0], $roleId);
                        }
                    }
                } else {
                    // Пользователь найден - обновляем данные
                    $profile = $customer->getOne('Profile');
                    if ($profile) {
                        $changed = false;
                        
                        if (!empty($phone) && $profile->get('mobilephone') != $phone) {
                            $profile->set('mobilephone', $phone);
                            $changed = true;
                        }
                        
                        if (!empty($email) && $profile->get('email') != $email) {
                            $profile->set('email', $email);
                            $changed = true;
                        }
                        
                        if (!empty($receiver) && $profile->get('fullname') != $receiver) {
                            $profile->set('fullname', $receiver);
                            $changed = true;
                        }
                        
                        if ($changed) {
                            $profile->save();
                        }
                    }
                }
            }
        }

        $response = $this->invokeEvent('msOnGetOrderCustomer', [
            'order' => $this->order,
            'customer' => $customer,
        ]);
        if (!$response['success']) {
            return $response['message'];
        }

        return $customer instanceof modUser ? $customer->get('id') : 0;
    }
        
}

согласно священописанию в консоле делаю так:
<?php
if ($miniShop2 = $modx->getService('miniShop2')) {
  $miniShop2->addService('order', 'my_OrderHandler',
      '{core_path}components/minishop2/custom/order/my_msorderhandler.class.php');
}
Пробую, и ничего не меняется. На пару с ИИ добрел до системной настройки, с ключем ms2_order_handler_class и там указал свой класс. Перестало работать вообще. Т.е. тыкаю на кнопку «сделать заказ» на самом сайте и ничего не происходит.
Из чего делаю вывод, что класс мой не зарегистрировался.

Дальше меняю настройку ms2_services.
она у меня выглядела так:
{«cart»:[],«order»:[«my_OrderHandler»],«payment»:[],«delivery»:{«mydelivery»:"{core_path}components\/minishop2\/custom\/delivery\/my_msdeliveryhandler.class.php"}}

т.к. я уже ранее подрубал собственную службу доставки, и она вроде как исправно работает, делаю по образу и подобию с order, хотя ИИ визжит, что так делать нельзя, и у этой настройки должно быть только название класса.

В логах живет вот такая ошибка:

(ERROR @ /home/c/cz42644/test-iflower/public_html/core/components/minishop2/model/minishop2/minishop2.class.php: 484) [miniShop2] Could not load custom class at "/home/c/cz42644/test-iflower/public_html/my_OrderHandler"
Т.е. я так понимаю, он ищет файл с моей службой почему-то не так где надо, по сути в корне. И как ему указать, где искать — не ведаю.

Вообщем, итог такой, что либо ничего не меняется, либо не работает вовсе.

Что и где я делаю не так?
Артур Шевченко
20 февраля 2026, 19:08
0
Вот тебе моё мнение, через полгода-год заказчикам будем всё равно на чём ты будешь делать сайт, гораздо больше их будет волновать вопрос: умеешь ли ты пользоваться ИИ-агентам? Именно ИИ позволяет ускорится в два-три раза и твой стэк уже не имеет значение, важно будет только умение решать бизнес-задачи и ставить задачи ИИ-агенту. Так что рекомендую начинать осваивать именно эти технологии.
Артур Шевченко
20 февраля 2026, 18:45
0
minishop3 недавно вышел, он ещё не прошёл обкатку и без опыта в разработке переезжать, наверное, не стоит. Многое из того, что тебе нужно @Николай Савин уже либо сделал, либо это у него в планах. Поддержать Николая можно донатом.

Отправить на карту СберБанка messenger.online.sberbank.ru/sl/kPLh3bewMs93eW9Hl
Отправить на карту Тинькофф www.tinkoff.ru/sl/2V9U9RrcJZP
Отправить на YooMoney yoomoney.ru/fundraise/RPkkYwNcL7A.230131
QIWI к сожалению недоступен до восстановления их банковской лицензии
Казахстан Каспи по номеру +7 701 282 77 37
Если Вы находитесь в Казахстане и хотите сделать пожертвование на счет ИП, можем сделать официальные документы (при условии адекватной суммы).

Крипта
USDT TRON (TRC20) TU3gzTp2Rt3wPcts1xiXtwWiu7riC8s56q
Bitcoin — BTC 1LxpXEBBCEQYoswu8tGo6TEU1C8JfmRT18
The Open Network — TON UQAeMteI6GWxyCan_QOXX8sKnDAr5ApF-9FK7f-g2qxVlz-e

Со своей стороны обещаю скорый выпуск интеграции со СДЕК.
Олег Захаров
20 февраля 2026, 16:01
0
Компонент очень нужный и мне кажется будет востребован.
У меня тут задача стоит сделать что-то подобное на сайте на движке на MODX 2.8 — там есть старые другие решения.
Но задумываюсь о глобальном переносе на 3-ю версию.
Олег Захаров
20 февраля 2026, 16:00
0
ну я подумал что возможно ты сам лично уже где-то у себя на заказе рабочем внедрил и есть рабочий сайт
Олег Захаров
20 февраля 2026, 15:58
0
А какой компонент для личного кабинета и авторизации используется?
Андрей
20 февраля 2026, 15:53
+1
вот нашел еще проще решение для текущего пользователя, без id
$profile = $modx->user->getOne('Profile');
return $profile->get('extended')[$input];
и вызов
[[!userField? &input=`tv_name`]]
но все равно хотелось бы понять причины и может есть штатное решение.