Создание CRUD интерфейса вне админки
Приветствую, в этой заметке хочу поделится кейсом по созданию простого интерфейса за пределами админки для управления различными объектами как нативными (пользователи, ресурсы, заказы), так и кастомными, т.е. созданными под конкретный проект. Видео с результатом
Задача.
Создать интерфейс для менеджеров, через который они смогу добавлять, изменять и удалять пользователей, а так же отправлять им приглашения на прохождение тестирования.
Компоненты.
Основной код будет располагаться в папке core/services/custom. Подключать наш сервис работы с пользователями будем с помощью composer. Для этого создадим в папке core/services файл composer.json со следующим содержимым
Использовать composer необязательно, можно расположить код где угодно и подключать классы через reqiure, но использование composer видится мне более удобным и актуальным способом загрузки классов.
Основная идея.
SendIt умеет отправлять данные на сервер и обрабатывать ответы от него. Значит наша задача сводится к тому, чтобы сообщить серверу какую операцию мы хотим выполнить: добавить пользователя, найти, изменить или удалить. Для простоты, операции создания и изменения объединим. Далее создадим 4 пресета ( четвёртый нужен чтобы не дублировать общие параметры)
В видео, которое выложено в начале заметки, не видно, но результаты всех вызовов возвращаются с разбивкой на страницы, т.е. работает постраничная навигация. Для этого в файле core/elements/templates/candidates.tpl есть вот такой вызов
А чтобы можно было искать пользователей, я добавил форму поиска
Таким образом основная идея состоит в том, чтобы писать код в одном классе-сервисе и обращаться к его методам через один универсальный сниппет. Логика разделения сервисов у каждого своя, главное не терять здравый смысл.
Дополнительная информация.
Весь код выложен в открытом репозитории. Кроме серверной части, там есть и немного стилей и JavaSrcipt. В части JavaScript следут обратить внимание на метод project.updatePagination() -обновляющий состояние пагинации после поиска поьзователя, и на метод project.insertData(e.target.closest('[data-insert]')), который обеспечивает вставку данных пользователя в скрытые поля для передачи на сервер.
Заключение.
На мой взгляд данный кейс легко экстрапалируется на любые объект, поэтому пользуйтесь, не стесняйтесь. Если есть вопросы или замечания, милости прошу в комментарии.
Всем щедрых заказчиков и качественного кода. Спасибо за внимание.
Задача.
Создать интерфейс для менеджеров, через который они смогу добавлять, изменять и удалять пользователей, а так же отправлять им приглашения на прохождение тестирования.
Компоненты.
- SendIt для отправки запросов и обработки ответов
- pdoTools для парсинга чанков
Основной код будет располагаться в папке core/services/custom. Подключать наш сервис работы с пользователями будем с помощью composer. Для этого создадим в папке core/services файл composer.json со следующим содержимым
{
"autoload": {
"psr-4": {
"CustomServices\\": "custom/"
}
}
}
Затем нужно запустить в терминале команду composer dump-autoload
После выполнения этой команды должна появиться папка services/vendor. На этом подготовительная часть завершена.Использовать composer необязательно, можно расположить код где угодно и подключать классы через reqiure, но использование composer видится мне более удобным и актуальным способом загрузки классов.
Основная идея.
SendIt умеет отправлять данные на сервер и обрабатывать ответы от него. Значит наша задача сводится к тому, чтобы сообщить серверу какую операцию мы хотим выполнить: добавить пользователя, найти, изменить или удалить. Для простоты, операции создания и изменения объединим. Далее создадим 4 пресета ( четвёртый нужен чтобы не дублировать общие параметры)
<?php
return [
'user' => [
'hooks' => '',
'snippet' => '@FILE snippets/manage_users.php',
],
'find_user' => [
'extends' => 'user',
'method' => 'findUser',
],
'remove_user' => [
'extends' => 'user',
'method' => 'removeUser',
],
'manage_user' => [
'clearFieldsOnSuccess' => 1,
'extends' => 'manage_users',
'method' => 'manageUser',
'successMessage' => 'Пользователь добавлен.',
'validate' => 'fullname:required,email:required,phone:required',
],
];
Как видите все пресеты используют один сниппет, но разные методы. Код сниппета выглядит так<?php
use CustomServices\Users;
require_once MODX_CORE_PATH . 'services/vendor/autoload.php';
/**
* @var modX $modx
* @var array $scriptProperties
* @var SendIt $SendIt
* @var string $method
*/
$service = new Users($modx);
$method = $scriptProperties['method'];
if(!method_exists($service, $method)){
return $SendIt->error('Метод '.$method.' не найден', []);
}
$result = $service->$method($_POST, $scriptProperties);
if($SendIt){
if($result['success']){
return $SendIt->success($result['message'], $result['data']);
}else{
return $SendIt->error($result['message'], $result['data']);
}
}
return $result;
В нём мы подключаем наш сервис работы с пользователем и выполняем переданный метод, в который передаём параметры запроса (массив $_POST) и параметры вызова ($scriptProperties). В зависимости от того как вызывается данный сниппет: в шаблоне или из JS, возвращаем разный результат. Таким образом мы получаем один универсальный сниппет, который можно вызывать как угодно. В видео, которое выложено в начале заметки, не видно, но результаты всех вызовов возвращаются с разбивкой на страницы, т.е. работает постраничная навигация. Для этого в файле core/elements/templates/candidates.tpl есть вот такой вызов
<ul class="list-unstyled border-end border-start border-1 border-secondary" data-pn-result>
{'!Pagination' | snippet: [
'snippet' => '!Pagination',
'render' => '@FILE snippets/manage_users.php',
'presetName' => 'pagination-search',
'tpl' => '@FILE chunks/get_candidates/item.tpl',
'tplEmpty' => '@INLINE <li class="row align-items-center border-bottom border-1 border-secondary mx-0 py-3">Кандидатов не найдено.</li>',
'limit' => 10,
'pagination' => 'candidates',
'resultBlockSelector' => '[data-pn-result]',
'resultShowMethod' => 'insert',
'method' => 'getListCandidates',
'hashParams' => 'query',
]}
</ul>
<!-- PAGINATION -->
{set $totalPages = 'candidates.totalPages' | placeholder}
{set $currentPage = 'candidates.currentPage' | placeholder}
{set $limit = 'candidates.limit' | placeholder}
<div data-pn-pagination="candidates" data-pn-type="" class="{$totalPages < 2 ? 'v_hidden' : ''} pagination">
<button type="button" data-pn-first="1">‹‹</button>
<button type="button" data-pn-prev="">‹</button>
<input type="number" name="candidatespage" data-pn-current data-si-preset="pagination-search" form="searchForm" min="1" max="{$totalPages}"
value="{$currentPage?:1}">
<p>из
<span data-pn-total="">{$totalPages?:1}</span>
</p>
<button type="button" data-pn-next="">›</button>
<button type="button" data-pn-last="{$totalPages}">››</button>
</div>
Как видите в этом вызове так же происходит обращение к нашему универсальному сниппету manage_users.php. А чтобы можно было искать пользователей, я добавил форму поиска
<form class="row" id="searchForm" data-si-form data-si-nosave data-si-preset="pagination-search">
<div class="col-6">
<input type="text" class="form-control" name="query" placeholder="ФИО/Email">
</div>
<div class="col-3">
<button type="submit" class="btn btn-primary w-100 d-block">Найти</button>
</div>
<div class="col-3">
<button type="reset" class="btn btn-outline-primary w-100 d-block" data-si-event="click">Сбросить</button>
</div>
</form>
ВАЖНО!!! Она использует тот же пресет, что и вызов сниппета Pagination, и связана с поле ввода номера текущей страницы с помощью атрибута form и собственного id.Таким образом основная идея состоит в том, чтобы писать код в одном классе-сервисе и обращаться к его методам через один универсальный сниппет. Логика разделения сервисов у каждого своя, главное не терять здравый смысл.
Дополнительная информация.
Весь код выложен в открытом репозитории. Кроме серверной части, там есть и немного стилей и JavaSrcipt. В части JavaScript следут обратить внимание на метод project.updatePagination() -обновляющий состояние пагинации после поиска поьзователя, и на метод project.insertData(e.target.closest('[data-insert]')), который обеспечивает вставку данных пользователя в скрытые поля для передачи на сервер.
Заключение.
На мой взгляд данный кейс легко экстрапалируется на любые объект, поэтому пользуйтесь, не стесняйтесь. Если есть вопросы или замечания, милости прошу в комментарии.
Всем щедрых заказчиков и качественного кода. Спасибо за внимание.
Поблагодарить автора
Отправить деньги
Комментарии: 7
Пользуюсь похожей конструкцией, чтобы редактировать TV-шки с фронта. Отличный пример допиливания полезного функционала под проект, спасибо!
P.S. Поправь плиз отступы в форматировании кода во 3 и 4 блоках, а то я shift+tab чуть не нажал машинально, когда читал :)
P.S. Поправь плиз отступы в форматировании кода во 3 и 4 блоках, а то я shift+tab чуть не нажал машинально, когда читал :)
Поправил)))
Здравствуйте.
Отличное и нужное решение. Обязательно воспользуюсь.
Но хотел уточнить не возникнет ли проблем при использовании MODX3 + php8.1?
И в плане безопасности можно ли использовать в «личном кабинете пользователя» (простые юзеры, которые залогинены в контексте web) данное решение? хотел кастомную таблицу подключить, чтобы юзеры меняли в ней данные.
Отличное и нужное решение. Обязательно воспользуюсь.
Но хотел уточнить не возникнет ли проблем при использовании MODX3 + php8.1?
И в плане безопасности можно ли использовать в «личном кабинете пользователя» (простые юзеры, которые залогинены в контексте web) данное решение? хотел кастомную таблицу подключить, чтобы юзеры меняли в ней данные.
Тоже интересно что с безопасностью. В коде на гитхабе никаких проверок на доступ. Возможно безопасность обеспечивает SendIt, но автор вообще не указал как SendIt подключается и используется. Наверно в SendIt подключаются прессеты??? В общем, для человека не знакомого с SendIt, как мне, статья вообще не понятная.
Безопасность может быть реализованна таким образом. Сниппет, типа AjaxForm, при обработки страницы, записывает в сессию хеш-код. js в браузере отправляет с формой этот хеш-код. Обработчик ajax запросов на сервере смотрит есть ли в сесии этот хеш-код и если его нет, то возращяет ошибку.
Таким образом, если сниппет не был вызван при обработке страницы, то все ajax запросы завершаться ошибкой. Будут приняты только те запросы с правильным хеш-кодом.
А на ресурс MODX можно поставить ограничения и показывать его только разрешенным пользователям. Соответственно на странице на запрещенном ресурсе сниппет не отработал, хеш-код в сессию не записался и ajax запросы не срабатывают.
В общем не знаю, как безопасность реализованна в SendIt, но наверно что-то подобное. Надеюсь @Артур Шевченко просветит нас :-)
Безопасность может быть реализованна таким образом. Сниппет, типа AjaxForm, при обработки страницы, записывает в сессию хеш-код. js в браузере отправляет с формой этот хеш-код. Обработчик ajax запросов на сервере смотрит есть ли в сесии этот хеш-код и если его нет, то возращяет ошибку.
Таким образом, если сниппет не был вызван при обработке страницы, то все ajax запросы завершаться ошибкой. Будут приняты только те запросы с правильным хеш-кодом.
А на ресурс MODX можно поставить ограничения и показывать его только разрешенным пользователям. Соответственно на странице на запрещенном ресурсе сниппет не отработал, хеш-код в сессию не записался и ajax запросы не срабатывают.
В общем не знаю, как безопасность реализованна в SendIt, но наверно что-то подобное. Надеюсь @Артур Шевченко просветит нас :-)
Кстати GRUD в getTables реализован и можно его использовать для подобной задачи :-). У пользователей MODX 2 таблицы и не так просто. Но можно попробовать.
На странице на которую ограничен доступ пишем сниппет:
Так же можно это же сделать с PVTables, но этот компонент я еще не добрался опубликовать :-(. И писать в 2 таблицы сразу там не сделанно. И события не все прописал :-(.
Безопасность обеспечивается хеш-кодом. Страница на которой опубликован сниппет должна быть доступна только нужным пользователям. Использовать getTables, для подобных задач, думаю, проше и быстрее чем писать контроллеры для sendIt.
На странице на которую ограничен доступ пишем сниппет:
{'!getTable' | snippet : [
'table'=>[
'event'=>1,
'class'=>'modUser',
'actions'=>[
'create'=>[],
'update'=>[],
'remove'=>[],
'export_excel'=>[],
],
'pdoTools'=>[
'class'=>'modUser',
'leftJoin'=>[
'modUserProfile'=>[
'class'=>'modUserProfile',
'on'=>'modUserProfile.internalKey = modUser.id'
]
],
'select'=>[
'modUser'=>'modUser.id',
'modUserProfile'=>'modUserProfile.fullname,modUserProfile.email,modUserProfile.phone',
],
'sortby'=>[
'modUser.id'=>'DESC'
]
],
'row'=>[
'id'=>['filter'=>1],
'password'=>[
'edit'=>['type'=>'hidden'],
'default'=>'Не секретный пароль'
],
'fullname'=>[
'class'=>'modUserProfile',
'label'=>'ФИО',
'edit'=>['type'=>'text','search_fields'=>['internalKey'=>'id'],],
'filter'=>1,
],
'email'=>[
'class'=>'modUserProfile',
'label'=>'email',
'edit'=>['type'=>'text','search_fields'=>['internalKey'=>'id'],],
'filter'=>1,
],
'phone'=>[
'class'=>'modUserProfile',
'label'=>'Телефон',
'edit'=>['type'=>'text','search_fields'=>['internalKey'=>'id'],],
'filter'=>1,
],
]
]
]}
Пишем плагин на событие getTablesAfterUpdateCreate. Для работы плагина в сниппете должно быть указано 'event'=>1, как выше.<?php
switch ($modx->event->name) {
case 'getTablesAfterUpdateCreate':
// $modx->log(1,'getTablesAfterUpdateCreate '.print_r($data,1));
if($data['table_name'] == 'modUser' and $data['gts_action'] == 'getTable/create'){
if($modUser = $modx->getObject('modUser',(int)$data['id'])){
$modUser->set('username',$data['email']);
$modUser->set('password',md5(date('d.m.Y H:i:s')));
$modUser->joinGroup(2);
$modUser->save();
}
}
break;
}
И все работает :-).Так же можно это же сделать с PVTables, но этот компонент я еще не добрался опубликовать :-(. И писать в 2 таблицы сразу там не сделанно. И события не все прописал :-(.
Безопасность обеспечивается хеш-кодом. Страница на которой опубликован сниппет должна быть доступна только нужным пользователям. Использовать getTables, для подобных задач, думаю, проше и быстрее чем писать контроллеры для sendIt.
Забыл. getTables работает в MODX2. В MODX3 тоже вроде пока работает, но там не гарантируется и приспосабливать getTables для MODX3 не планируется. Работу с произвольной таблицей в MODX3 вроде сильно усложнили и не понятно как сделать правильно.
Еще сейчас заметил у Артура редактируется выбранная группа пользователей. Чтоб редактировать группу 2 в gettables надо добавить в секцию pdoTools where:
'where'=>[
'modUser.primary_group' => 2,
'modUser.active' => 1,
]
Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.