@maxisoft/instantcms-mcp 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +308 -0
  3. package/dist/__tests__/artifact-tool.test.js +196 -0
  4. package/dist/__tests__/db-tool.test.js +152 -0
  5. package/dist/__tests__/define-tool.test.js +106 -0
  6. package/dist/__tests__/find-tool.test.js +185 -0
  7. package/dist/__tests__/get-component-api.test.js +144 -0
  8. package/dist/__tests__/hardening.test.js +73 -0
  9. package/dist/__tests__/hooks-tool.test.js +173 -0
  10. package/dist/__tests__/mcp-integration-extra.test.js +166 -0
  11. package/dist/__tests__/mcp-integration.test.js +53 -0
  12. package/dist/__tests__/pagination.test.js +114 -0
  13. package/dist/__tests__/performance-baseline.test.js +56 -0
  14. package/dist/__tests__/project-patch.test.js +144 -0
  15. package/dist/__tests__/project-source.test.js +44 -0
  16. package/dist/__tests__/project-workflows-extra.test.js +165 -0
  17. package/dist/__tests__/project-workflows.test.js +51 -0
  18. package/dist/__tests__/scaffold-addon-roundtrip.test.js +287 -0
  19. package/dist/__tests__/serialization.test.js +170 -0
  20. package/dist/__tests__/source-knowledge-parsers.test.js +42 -0
  21. package/dist/__tests__/template-development.test.js +78 -0
  22. package/dist/__tests__/template-productivity.test.js +87 -0
  23. package/dist/__tests__/tools.test.js +2143 -0
  24. package/dist/data/components.js +2818 -0
  25. package/dist/data/controllers-map.js +7571 -0
  26. package/dist/data/core-api.js +9356 -0
  27. package/dist/data/database-schema.js +4555 -0
  28. package/dist/data/events-map.js +1339 -0
  29. package/dist/data/fields-map.js +3692 -0
  30. package/dist/data/hooks.js +2257 -0
  31. package/dist/data/js-api.js +123 -0
  32. package/dist/data/libs-api.js +264 -0
  33. package/dist/data/routes-map.js +203 -0
  34. package/dist/data/schemas-validation.js +255 -0
  35. package/dist/data/schemas.js +1404 -0
  36. package/dist/data/traits-map.js +1004 -0
  37. package/dist/data/version-profiles.js +41 -0
  38. package/dist/data/widgets-map.js +131 -0
  39. package/dist/data/wysiwyg-map.js +486 -0
  40. package/dist/generated/components-source.js +14748 -0
  41. package/dist/generated/hooks-source.js +3399 -0
  42. package/dist/generated/knowledge-meta.js +101 -0
  43. package/dist/index.js +13 -0
  44. package/dist/registry/database-tools.js +112 -0
  45. package/dist/registry/extension-tools.js +473 -0
  46. package/dist/registry/generator-tools.js +506 -0
  47. package/dist/registry/knowledge-tools.js +441 -0
  48. package/dist/registry/language-tools.js +99 -0
  49. package/dist/registry/meta-tools.js +152 -0
  50. package/dist/registry/project-tools.js +48 -0
  51. package/dist/registry/resources.js +98 -0
  52. package/dist/registry/source-tools.js +211 -0
  53. package/dist/registry/template-development-tools.js +79 -0
  54. package/dist/server.js +32 -0
  55. package/dist/tools/addon-tool.js +1437 -0
  56. package/dist/tools/admin-partial-tool.js +498 -0
  57. package/dist/tools/api-tool.js +294 -0
  58. package/dist/tools/artifact-tool.js +95 -0
  59. package/dist/tools/cache-tool.js +360 -0
  60. package/dist/tools/component-tool.js +415 -0
  61. package/dist/tools/controllers-tool.js +125 -0
  62. package/dist/tools/cron-tool.js +197 -0
  63. package/dist/tools/crud-tool.js +659 -0
  64. package/dist/tools/db-tool.js +198 -0
  65. package/dist/tools/email-tool.js +222 -0
  66. package/dist/tools/external-api-tool.js +596 -0
  67. package/dist/tools/filter-tool.js +341 -0
  68. package/dist/tools/form-tool.js +275 -0
  69. package/dist/tools/grid-tool.js +189 -0
  70. package/dist/tools/hooks-tool.js +104 -0
  71. package/dist/tools/import-export-tool.js +548 -0
  72. package/dist/tools/lang-tool.js +251 -0
  73. package/dist/tools/layout-override-tool.js +125 -0
  74. package/dist/tools/layout-tool.js +548 -0
  75. package/dist/tools/maria-tool.js +127 -0
  76. package/dist/tools/mariadb.js +201 -0
  77. package/dist/tools/migration-tool.js +329 -0
  78. package/dist/tools/oauth-tool.js +520 -0
  79. package/dist/tools/parser/components-parser.js +76 -0
  80. package/dist/tools/parser/controllers-parser.js +313 -0
  81. package/dist/tools/parser/core-parser.js +294 -0
  82. package/dist/tools/parser/coverage-generator.js +424 -0
  83. package/dist/tools/parser/events-parser.js +127 -0
  84. package/dist/tools/parser/fields-parser.js +382 -0
  85. package/dist/tools/parser/hooks-parser.js +106 -0
  86. package/dist/tools/parser/sql-parser.js +197 -0
  87. package/dist/tools/parser/traits-parser.js +161 -0
  88. package/dist/tools/parser/widgets-parser.js +150 -0
  89. package/dist/tools/permission-tool.js +348 -0
  90. package/dist/tools/project-patch-tool.js +73 -0
  91. package/dist/tools/project-source-tool.js +188 -0
  92. package/dist/tools/project-workflow-tool.js +186 -0
  93. package/dist/tools/requirement-tool.js +212 -0
  94. package/dist/tools/scaffold-tool.js +815 -0
  95. package/dist/tools/seo-tool.js +412 -0
  96. package/dist/tools/source-tool.js +155 -0
  97. package/dist/tools/template-development-tool.js +271 -0
  98. package/dist/tools/template-overrides-tool.js +294 -0
  99. package/dist/tools/template-productivity-tool.js +319 -0
  100. package/dist/tools/template-tool.js +665 -0
  101. package/dist/tools/test-tool.js +183 -0
  102. package/dist/tools/webhook-tool.js +427 -0
  103. package/dist/tools/widget-tool.js +331 -0
  104. package/dist/tools/wysiwyg-tool.js +137 -0
  105. package/dist/types/scaffold.js +68 -0
  106. package/dist/utils/define-tool.js +37 -0
  107. package/dist/utils/find-tool.js +97 -0
  108. package/dist/utils/mcp-result.js +18 -0
  109. package/dist/utils/pagination.js +27 -0
  110. package/dist/utils/serialization.js +27 -0
  111. package/package.json +94 -0
@@ -0,0 +1,1404 @@
1
+ "use strict";
2
+ // Структуры файлов дополнений и шаблонов InstantCMS
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.fieldTypes = exports.templateStructure = exports.addonStructures = exports.classNamingConventions = exports.controllerDirectoryLayout = void 0;
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // ПОЛНАЯ СТРУКТУРА ПАПКИ КОНТРОЛЛЕРА
7
+ // /system/controllers/{name}/
8
+ // ─────────────────────────────────────────────────────────────────────────────
9
+ exports.controllerDirectoryLayout = `
10
+ ПОЛНАЯ СТРУКТУРА ПАПКИ КОНТРОЛЛЕРА
11
+ ===================================
12
+ /system/controllers/{name}/
13
+ ├── frontend.php ← class {name} extends cmsFrontend
14
+ ├── backend.php ← class backend{Name} extends cmsBackend
15
+ ├── model.php ← class model{Name} extends cmsModel
16
+ ├── manifest.xml ← метаданные, хуки, зависимости
17
+ ├── install.php ← class install{Name} extends cmsInstaller
18
+ ├── uninstall.php ← class uninstall{Name} extends cmsInstaller
19
+ ├── routes.php ← function routes_{name}() { return [...] }
20
+ ├── compatibility.php ← trait для обратной совместимости (опц.)
21
+
22
+ ├── actions/ ← ФРОНТЕНД экшены
23
+ │ ├── index.php ← class action{Name}Index extends cmsAction
24
+ │ ├── view.php ← class action{Name}View extends cmsAction
25
+ │ └── ... ← один файл = один экшен
26
+
27
+ ├── backend/ ← БЭКЕНД компоненты
28
+ │ ├── model.php ← class modelBackend{Name} extends model{Name}
29
+ │ ├── actions/ ← БЭКЕНД экшены (отдельные файлы)
30
+ │ │ ├── index.php ← class actionBillingIndex extends cmsAction { run() }
31
+ │ │ ├── items.php ← использует trait listgrid
32
+ │ │ ├── items_add.php ← использует trait formItem
33
+ │ │ └── ...
34
+ │ ├── forms/ ← ФОРМЫ для бэкенда
35
+ │ │ ├── form_item.php ← class form{Name}Item extends cmsForm
36
+ │ │ ├── form_options.php ← class form{Name}Options extends cmsForm
37
+ │ │ └── ...
38
+ │ └── grids/ ← ГРИДЫ для списков в бэкенде
39
+ │ ├── grid_items.php ← function grid_items($controller) { return [...] }
40
+ │ └── ... ← ФУНКЦИИ, не классы!
41
+
42
+ ├── hooks/ ← ХУКИ
43
+ │ ├── content_after_add_approve.php ← class on{Name}ContentAfterAddApprove extends cmsAction
44
+ │ └── ... ← имя файла = имя хука
45
+
46
+ ├── widgets/ ← ВИДЖЕТЫ для публичных страниц
47
+ │ └── {widget_name}/
48
+ │ ├── widget.php ← class widget{Name}{WidgetName} extends cmsWidget
49
+ │ └── options.form.php ← class formWidget{Name}{WidgetName}Options extends cmsForm
50
+
51
+ └── traits/ ← ТРЕЙТЫ (опционально)
52
+ └── validation.php
53
+
54
+ ЯЗЫКОВЫЕ ФАЙЛЫ (ВНЕ папки контроллера!):
55
+ /system/languages/ru/controllers/{name}/{name}.php
56
+ /system/languages/en/controllers/{name}/{name}.php
57
+
58
+ ШАБЛОНЫ (в папке frontend-темы, НЕ в контроллере!):
59
+ /templates/{theme_name}/controllers/{name}/{action}.tpl.php ← фронтенд-экшен
60
+ /templates/{theme_name}/controllers/{name}/backend/{action}.tpl.php ← бэкенд-экшен (подпапка backend/)
61
+ /templates/{theme_name}/controllers/{name}/widgets/{w}/{w}.tpl.php ← виджет контроллера
62
+
63
+ ВАЖНО: бэкенд-шаблоны ВСЕГДА в папке frontend-темы (modern/), НЕ в admincoreui/
64
+ admincoreui/ предоставляет ТОЛЬКО layout-оболочку (navbar/sidebar через admin.tpl.php)
65
+
66
+ ПАКЕТ ДЛЯ УСТАНОВКИ ЧЕРЕЗ МЕНЕДЖЕР ДОПОЛНЕНИЙ:
67
+ {name}.zip
68
+ ├── manifest.ru.ini ← метаданные пакета
69
+ ├── install.sql ← SQL для создания таблиц
70
+ └── package/
71
+ └── system/
72
+ ├── controllers/{name}/ ← все файлы контроллера
73
+ └── languages/ru/controllers/{name}/{name}.php
74
+ `;
75
+ // ─────────────────────────────────────────────────────────────────────────────
76
+ // ИМЕНОВАНИЕ КЛАССОВ — КРИТИЧЕСКИ ВАЖНО
77
+ // ─────────────────────────────────────────────────────────────────────────────
78
+ exports.classNamingConventions = `
79
+ ПРАВИЛА ИМЕНОВАНИЯ КЛАССОВ
80
+ ===========================
81
+ frontend.php: class {name} extends cmsFrontend
82
+ backend.php: class backend{Name} extends cmsBackend
83
+ model.php: class model{Name} extends cmsModel
84
+ backend/model.php: class modelBackend{Name} extends model{Name}
85
+
86
+ Фронтенд экшены (actions/*.php):
87
+ actions/index.php → class action{Name}Index extends cmsAction
88
+ actions/view.php → class action{Name}View extends cmsAction
89
+ Формат: action + PascalCase(controller) + PascalCase(action_name)
90
+
91
+ Бэкенд экшены (backend/actions/*.php):
92
+ backend/actions/items.php → class action{Name}Items extends cmsAction { use listgrid; }
93
+ backend/actions/items_add.php → class action{Name}ItemsAdd extends cmsAction { use formItem; }
94
+ Формат: action + PascalCase(controller) + PascalCase(action_name)
95
+
96
+ Формы (backend/forms/*.php):
97
+ backend/forms/form_item.php → class form{Name}Item extends cmsForm
98
+ backend/forms/form_options.php → class form{Name}Options extends cmsForm
99
+
100
+ Гриды (backend/grids/*.php):
101
+ backend/grids/grid_items.php → function grid_items($controller, $model=null) { return [...] }
102
+ ВАЖНО: Гриды — это ФУНКЦИИ, не классы!
103
+
104
+ Хуки (hooks/*.php):
105
+ hooks/content_after_add_approve.php → class on{Name}ContentAfterAddApprove extends cmsAction
106
+ Формат: on + PascalCase(controller) + PascalCase(hook_name)
107
+
108
+ Виджеты (widgets/{widget_name}/widget.php):
109
+ → class widget{Name}{WidgetName} extends cmsWidget
110
+ Пример: class widgetBillingPlans extends cmsWidget
111
+
112
+ Формы опций виджетов (widgets/{widget_name}/options.form.php):
113
+ → class formWidget{Name}{WidgetName}Options extends cmsForm
114
+
115
+ Установщик:
116
+ install.php → class install{Name} extends cmsInstaller
117
+ uninstall.php → class uninstall{Name} extends cmsInstaller
118
+
119
+ Примеры для addon name = "catalog":
120
+ frontend.php → class catalog extends cmsFrontend
121
+ backend.php → class backendCatalog extends cmsBackend
122
+ model.php → class modelCatalog extends cmsModel
123
+ backend/actions/items.php → class actionCatalogItems extends cmsAction
124
+ backend/actions/items_add.php → class actionCatalogItemsAdd extends cmsAction
125
+ backend/forms/form_item.php → class formCatalogItem extends cmsForm
126
+ backend/grids/grid_items.php → function grid_items($controller) { return [...] }
127
+ hooks/user_registered.php → class onCatalogUserRegistered extends cmsAction
128
+ widgets/list/widget.php → class widgetCatalogList extends cmsWidget
129
+ `;
130
+ exports.addonStructures = {
131
+ basic: {
132
+ type: "basic",
133
+ description: "Минимальное дополнение — только фронтенд без админ-панели",
134
+ notes: [
135
+ "Языковой файл расположен ВНЕ папки контроллера: /system/languages/ru/controllers/{name}/{name}.php",
136
+ "Шаблоны расположены в /templates/{theme}/controllers/{name}/{action}.tpl.php",
137
+ "Экшены — отдельные файлы в actions/*.php, один файл = один экшен",
138
+ "frontend.php использует route() + runAction(), не содержит встроенных методов-экшенов"
139
+ ],
140
+ files: [
141
+ {
142
+ path: "frontend.php",
143
+ required: true,
144
+ description: "Основной контроллер фронтенда. Наследует cmsFrontend. Содержит только route() + бизнес-логику. Экшены вынесены в отдельные файлы actions/*.php.",
145
+ template: `<?php
146
+ /**
147
+ * @property \\model{Name} $model
148
+ */
149
+ class {name} extends cmsFrontend {
150
+
151
+ protected $useOptions = true;
152
+
153
+ // route() вызывается системой для каждого запроса
154
+ // parseRoute() разбирает URI и определяет экшен
155
+ // runAction() запускает найденный экшен из actions/{action}.php
156
+
157
+ }`
158
+ },
159
+ {
160
+ path: "actions/index.php",
161
+ required: true,
162
+ description: "Экшен главной страницы. Имя класса: action{Name}Index. Все экшены наследуют cmsAction и имеют метод run().",
163
+ template: `<?php
164
+
165
+ class action{Name}Index extends cmsAction {
166
+
167
+ public function run() {
168
+
169
+ $page = $this->request->get('page', 1);
170
+ $perpage = $this->options['perpage'] ?? 10;
171
+
172
+ // $this->model — модель текущего дополнения
173
+ // $this->options — настройки дополнения
174
+ // $this->cms_user — текущий пользователь
175
+ // $this->cms_template — объект шаблона
176
+
177
+ $items = $this->model
178
+ ->filterEqual('is_pub', 1)
179
+ ->orderBy('date_pub', 'desc')
180
+ ->limit(($page - 1) * $perpage, $perpage)
181
+ ->get('{name}_items');
182
+
183
+ $total = $this->model->getCount('{name}_items');
184
+
185
+ // render() ищет шаблон в /templates/{theme}/controllers/{name}/index.tpl.php
186
+ return $this->cms_template->render([
187
+ 'items' => $items,
188
+ 'total' => $total,
189
+ 'page' => $page,
190
+ 'perpage' => $perpage
191
+ ]);
192
+ }
193
+
194
+ }`
195
+ },
196
+ {
197
+ path: "actions/view.php",
198
+ required: false,
199
+ description: "Экшен просмотра одного элемента",
200
+ template: `<?php
201
+
202
+ class action{Name}View extends cmsAction {
203
+
204
+ public function run($id = 0) {
205
+
206
+ $item = $this->model->getItemById('{name}_items', $id);
207
+ if (!$item) {
208
+ return cmsCore::error404();
209
+ }
210
+
211
+ $this->cms_template->setTitle($item['title']);
212
+ $this->cms_template->addBreadcrumb(LANG_{NAME}_TITLE, href_to('{name}'));
213
+ $this->cms_template->addBreadcrumb($item['title']);
214
+
215
+ return $this->cms_template->render([
216
+ 'item' => $item
217
+ ]);
218
+ }
219
+
220
+ }`
221
+ },
222
+ {
223
+ path: "model.php",
224
+ required: true,
225
+ description: "Модель базы данных. Наследует cmsModel. Содержит специфичные запросы для контроллера.",
226
+ template: `<?php
227
+
228
+ class model{Name} extends cmsModel {
229
+
230
+ // Фильтры применяются цепочкой (Fluent Interface)
231
+ // get() выполняет запрос и возвращает массив записей
232
+ // getItem() возвращает одну запись
233
+ // getCount() возвращает количество записей
234
+
235
+ public function getPublished($limit = 10) {
236
+ return $this->filterEqual('is_pub', 1)
237
+ ->orderBy('date_pub', 'desc')
238
+ ->limit($limit)
239
+ ->get('{name}_items');
240
+ }
241
+
242
+ public function getItemById($table, $id) {
243
+ return $this->filterEqual('id', $id)
244
+ ->getItem($table);
245
+ }
246
+
247
+ }`
248
+ },
249
+ {
250
+ path: "[ВНЕШНИЙ] system/languages/ru/controllers/{name}/{name}.php",
251
+ required: true,
252
+ description: "Языковой файл. ВАЖНО: находится ВНЕ папки контроллера, в /system/languages/{lang}/controllers/{name}/",
253
+ template: `<?php
254
+ // Файл: /system/languages/ru/controllers/{name}/{name}.php
255
+ // ВАЖНО: файл расположен в /system/languages/, а НЕ в /system/controllers/!
256
+
257
+ define('LANG_{NAME}_TITLE', 'Моё дополнение');
258
+ define('LANG_{NAME}_ADD', 'Добавить');
259
+ define('LANG_{NAME}_EDIT', 'Редактировать');
260
+ define('LANG_{NAME}_DELETE', 'Удалить');
261
+ define('LANG_{NAME}_NOT_FOUND', 'Ничего не найдено');
262
+
263
+ // Константы для бэкенда (префикс _CP_)
264
+ define('LANG_{NAME}_CP_TITLE', 'Моё дополнение');
265
+ define('LANG_{NAME}_CP_ITEMS', 'Элементы');
266
+ define('LANG_{NAME}_CP_ADD', 'Добавить элемент');`
267
+ },
268
+ {
269
+ path: "manifest.xml",
270
+ required: true,
271
+ description: "Метаданные дополнения: название, версия, зависимости, хуки",
272
+ template: `<?xml version="1.0" encoding="utf-8"?>
273
+ <addon>
274
+ <name>{name}</name>
275
+ <title>{Title}</title>
276
+ <description>Описание дополнения</description>
277
+ <version>1.0.0</version>
278
+ <build>1</build>
279
+ <author>
280
+ <name>Author Name</name>
281
+ <url>https://example.com</url>
282
+ <email>author@example.com</email>
283
+ </author>
284
+ <dependencies>
285
+ <!-- <controller name="content" /> -->
286
+ </dependencies>
287
+ <hooks>
288
+ <!-- Регистрация хуков: -->
289
+ <!-- <hook controller="{name}" name="content_after_add_approve" /> -->
290
+ </hooks>
291
+ </addon>`
292
+ },
293
+ {
294
+ path: "install.php",
295
+ required: true,
296
+ description: "Скрипт установки. Создаёт таблицы БД через $this->db->query().",
297
+ template: `<?php
298
+
299
+ class install{Name} extends cmsInstaller {
300
+
301
+ public function install() {
302
+
303
+ $this->db->query("CREATE TABLE IF NOT EXISTS \`{prefix}{name}_items\` (
304
+ \`id\` int(10) unsigned NOT NULL AUTO_INCREMENT,
305
+ \`user_id\` int(10) unsigned NOT NULL DEFAULT '0',
306
+ \`title\` varchar(255) NOT NULL DEFAULT '',
307
+ \`text\` text,
308
+ \`date_pub\` datetime NOT NULL,
309
+ \`is_pub\` tinyint(1) unsigned NOT NULL DEFAULT '1',
310
+ PRIMARY KEY (\`id\`),
311
+ KEY \`user_id\` (\`user_id\`),
312
+ KEY \`is_pub\` (\`is_pub\`)
313
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
314
+
315
+ return true;
316
+ }
317
+
318
+ }`
319
+ },
320
+ {
321
+ path: "uninstall.php",
322
+ required: true,
323
+ description: "Скрипт удаления. Удаляет таблицы и данные.",
324
+ template: `<?php
325
+
326
+ class uninstall{Name} extends cmsInstaller {
327
+
328
+ public function uninstall() {
329
+ $this->db->query("DROP TABLE IF EXISTS \`{prefix}{name}_items\`");
330
+ return true;
331
+ }
332
+
333
+ }`
334
+ }
335
+ ]
336
+ },
337
+ with_admin: {
338
+ type: "with_admin",
339
+ description: "Дополнение с полноценной админ-панелью: backend.php + backend/actions/ + backend/grids/ + backend/forms/",
340
+ notes: [
341
+ "backend.php содержит getBackendMenu() и before() — НЕ содержит методы-экшены",
342
+ "Каждый бэкенд экшен — отдельный файл в backend/actions/",
343
+ "Списки используют trait listgrid: table_name + grid_name + tool_buttons в __construct()",
344
+ "Формы используют trait formItem: table_name + form_name + success_url в __construct()",
345
+ "Гриды — ФУНКЦИИ (не классы): function grid_{name}($controller) { return ['options'=>..., 'columns'=>..., 'actions'=>...] }",
346
+ "Формы для бэкенда — КЛАССЫ: class form{Name}Item extends cmsForm { public function init($do, ...) {...} }"
347
+ ],
348
+ files: [
349
+ {
350
+ path: "frontend.php",
351
+ required: true,
352
+ description: "Фронтенд контроллер (см. basic)",
353
+ template: `// Аналогично basic, см. выше`
354
+ },
355
+ {
356
+ path: "backend.php",
357
+ required: true,
358
+ description: "Контроллер бэкенда. Содержит getBackendMenu(), before(), опции. НЕ содержит inline экшены — они в backend/actions/.",
359
+ template: `<?php
360
+ /**
361
+ * @property \\modelBackend{Name} $model
362
+ */
363
+ class backend{Name} extends cmsBackend {
364
+
365
+ // Автоматически генерирует экшен 'options' для формы настроек
366
+ public $useDefaultOptionsAction = true;
367
+
368
+ // Загружать настройки дополнения
369
+ protected $useOptions = true;
370
+
371
+ public function __construct(cmsRequest $request) {
372
+ parent::__construct($request);
373
+ // Передать настройки в модель, если нужно:
374
+ // $this->model->setControllerOptions($this->options);
375
+ }
376
+
377
+ // Вызывается перед каждым экшеном бэкенда
378
+ public function before($action_name) {
379
+ if (!parent::before($action_name)) {
380
+ return false;
381
+ }
382
+ // Дополнительные проверки доступа
383
+ return true;
384
+ }
385
+
386
+ // Меню в левой панели бэкенда
387
+ public function getBackendMenu() {
388
+ return [
389
+ [
390
+ 'title' => LANG_{NAME}_CP_ITEMS,
391
+ 'url' => href_to($this->root_url, 'items'),
392
+ 'options' => ['icon' => 'list']
393
+ ],
394
+ [
395
+ 'title' => LANG_OPTIONS,
396
+ 'url' => href_to($this->root_url, 'options'),
397
+ 'options' => ['icon' => 'cog']
398
+ ],
399
+ // Пункт с счётчиком (pending items):
400
+ // [
401
+ // 'title' => LANG_{NAME}_CP_PENDING,
402
+ // 'counter' => $this->model->getPendingCount(),
403
+ // 'url' => href_to($this->root_url, 'pending'),
404
+ // 'options' => ['icon' => 'clock']
405
+ // ],
406
+ // Пункт с подменю (childs_count = количество дочерних):
407
+ // [
408
+ // 'title' => LANG_{NAME}_CP_PRICES,
409
+ // 'url' => href_to($this->root_url, 'prices'),
410
+ // 'childs_count' => 3,
411
+ // 'options' => ['icon' => 'money-bill']
412
+ // ],
413
+ // [
414
+ // 'title' => LANG_{NAME}_CP_PRICES_BASIC,
415
+ // 'level' => 2,
416
+ // 'url' => href_to($this->root_url, 'prices', 'basic')
417
+ // ],
418
+ ];
419
+ }
420
+
421
+ }`
422
+ },
423
+ {
424
+ path: "backend/model.php",
425
+ required: false,
426
+ description: "Расширение модели для бэкенда. Добавляет JOIN-ы и агрегации для отображения в гридах.",
427
+ template: `<?php
428
+ /**
429
+ * Расширение модели для нужд бэкенда
430
+ */
431
+ class modelBackend{Name} extends model{Name} {
432
+
433
+ // Статистика для дашборда бэкенда
434
+ public function getStats() {
435
+ return [
436
+ 'total' => $this->getCount('{name}_items'),
437
+ 'published' => $this->filterEqual('is_pub', 1)->getCount('{name}_items'),
438
+ ];
439
+ }
440
+
441
+ // Список с дополнительными полями (JOIN с users)
442
+ public function getItemsWithUsers() {
443
+ return $this->joinLeft('users u', 'u.id = t.user_id', ['u.nickname as user_nickname'])
444
+ ->get('{name}_items');
445
+ }
446
+
447
+ }`
448
+ },
449
+ {
450
+ path: "backend/actions/items.php",
451
+ required: true,
452
+ description: "Бэкенд экшен — список элементов. Использует trait listgrid. Вся логика через свойства в __construct().",
453
+ template: `<?php
454
+
455
+ class action{Name}Items extends cmsAction {
456
+
457
+ use icms\\traits\\controllers\\actions\\listgrid;
458
+
459
+ public function __construct($controller, $params = []) {
460
+
461
+ parent::__construct($controller, $params);
462
+
463
+ // Обязательные свойства трейта listgrid:
464
+ $this->table_name = '{name}_items';
465
+ $this->grid_name = 'items'; // соответствует backend/grids/grid_items.php
466
+ $this->title = LANG_{NAME}_CP_ITEMS;
467
+
468
+ // Кнопка "Добавить" в тулбаре
469
+ $this->tool_buttons = [
470
+ [
471
+ 'class' => 'add',
472
+ 'title' => LANG_{NAME}_CP_ADD,
473
+ 'href' => $this->cms_template->href_to('items', 'add')
474
+ ]
475
+ ];
476
+
477
+ // Дополнительная обработка модели (JOIN-ы, фильтры):
478
+ $this->list_callback = function (\\cmsModel $model) {
479
+ return $model->joinLeft('users u', 'u.id = t.user_id', ['u.nickname as user_nickname']);
480
+ };
481
+
482
+ // Обработка после получения данных:
483
+ // $this->items_callback = function ($items) { return $items; };
484
+
485
+ // Префикс для вложенных экшенов (items_add → prefix 'items_')
486
+ $this->external_action_prefix = 'items_';
487
+ }
488
+
489
+ }`
490
+ },
491
+ {
492
+ path: "backend/actions/items_add.php",
493
+ required: true,
494
+ description: "Бэкенд экшен — добавление/редактирование. Использует trait formItem. Обрабатывает и add, и edit через один файл.",
495
+ template: `<?php
496
+
497
+ class action{Name}ItemsAdd extends cmsAction {
498
+
499
+ use icms\\traits\\controllers\\actions\\formItem;
500
+
501
+ public function __construct($controller, $params = []) {
502
+
503
+ parent::__construct($controller, $params);
504
+
505
+ $list_url = $this->cms_template->href_to('items');
506
+
507
+ // Обязательные свойства трейта formItem:
508
+ $this->table_name = '{name}_items';
509
+ $this->form_name = 'item'; // соответствует backend/forms/form_item.php
510
+ $this->success_url = $list_url;
511
+
512
+ // Заголовок: строка или массив ['add' => '...', 'edit' => '{title}']
513
+ $this->title = [
514
+ 'add' => LANG_{NAME}_CP_ADD,
515
+ 'edit' => '{title}' // {title} заменяется значением поля title записи
516
+ ];
517
+
518
+ // Хлебные крошки
519
+ $this->breadcrumbs = [
520
+ [LANG_{NAME}_CP_ITEMS, $list_url],
521
+ isset($params[0]) ? '{title}' : LANG_{NAME}_CP_ADD
522
+ ];
523
+
524
+ // Показать стандартные кнопки Сохранить/Отменить
525
+ $this->use_default_tool_buttons = true;
526
+
527
+ // Значения по умолчанию для новой записи:
528
+ // $this->default_item = ['is_pub' => 1];
529
+
530
+ // Коллбэк после добавления: function($id, $data) {...}
531
+ // $this->add_callback = function($id, $data) { ... };
532
+
533
+ // Коллбэк после обновления: function($data) {...}
534
+ // $this->update_callback = function($data) { ... };
535
+ }
536
+
537
+ }`
538
+ },
539
+ {
540
+ path: "backend/actions/index.php",
541
+ required: false,
542
+ description: "Дашборд бэкенда (главная страница раздела). Кастомный экшен без трейтов.",
543
+ template: `<?php
544
+ /**
545
+ * @property \\modelBackend{Name} $model
546
+ */
547
+ class action{Name}Index extends cmsAction {
548
+
549
+ public function run($do = false) {
550
+
551
+ // Передача на вложенный экшен: /admin/{name}/index/something
552
+ if ($do) {
553
+ return $this->runAction('index_' . $do, array_slice($this->params, 1));
554
+ }
555
+
556
+ $stats = $this->model->getStats();
557
+
558
+ // cms_template->render() ищет шаблон:
559
+ // /templates/{frontend-theme}/controllers/{name}/backend/index.tpl.php
560
+ // или системный:
561
+ // /system/controllers/{name}/templates/index.tpl.php
562
+ return $this->cms_template->render([
563
+ 'stats' => $stats,
564
+ ]);
565
+ }
566
+
567
+ }`
568
+ },
569
+ {
570
+ path: "backend/grids/grid_items.php",
571
+ required: true,
572
+ description: "Определение грида для списка. ФУНКЦИЯ, не класс. Возвращает массив с options, columns, actions.",
573
+ template: `<?php
574
+ // ВАЖНО: грид — это ФУНКЦИЯ, не класс!
575
+ // Имя функции = grid_ + имя грида (то, что указано в $this->grid_name)
576
+
577
+ function grid_items($controller) {
578
+
579
+ $options = [
580
+ 'is_sortable' => true, // Сортировка по столбцам
581
+ 'is_filter' => true, // Панель фильтров
582
+ 'is_pagination' => true, // Пагинация
583
+ 'is_draggable' => false, // Перетаскивание строк (для ordering)
584
+ 'is_selectable' => false, // Чекбоксы для массовых операций
585
+ 'order_by' => 'id', // Поле сортировки по умолчанию
586
+ 'order_to' => 'desc', // Направление (asc/desc)
587
+ 'show_id' => true // Показать колонку ID
588
+ // 'drag_save_url' => href_to('admin', 'reorder', '{name}_items'),
589
+ ];
590
+
591
+ $columns = [
592
+ // Простая колонка
593
+ 'id' => [
594
+ 'title' => 'ID',
595
+ 'width' => 60
596
+ ],
597
+ // Колонка со ссылкой на редактирование
598
+ 'title' => [
599
+ 'title' => LANG_TITLE,
600
+ 'href' => href_to($controller->root_url, 'items', ['edit', '{id}']),
601
+ 'order_by' => 't.title', // для JOIN-ов указывайте алиас таблицы
602
+ 'filter' => 'like' // текстовый поиск
603
+ ],
604
+ // Колонка с JOIN-ом (user_nickname из LEFT JOIN users)
605
+ 'user_nickname' => [
606
+ 'title' => LANG_USER,
607
+ 'width' => 150,
608
+ 'href' => href_to('users', '{user_id}'),
609
+ 'order_by' => 'u.nickname',
610
+ 'filter' => 'like',
611
+ 'filter_by' => 'u.nickname'
612
+ ],
613
+ // Дата с форматированием
614
+ 'date_pub' => [
615
+ 'title' => LANG_DATE_PUB,
616
+ 'width' => 150,
617
+ 'filter' => 'date',
618
+ 'handler' => function ($value) {
619
+ return html_date_time($value);
620
+ }
621
+ ],
622
+ // Флаг с переключателем inline
623
+ 'is_pub' => [
624
+ 'title' => LANG_IS_PUB,
625
+ 'width' => 60,
626
+ 'flag' => true,
627
+ 'flag_toggle' => href_to($controller->root_url, 'toggle_item', ['{id}', '{name}_items', 'is_pub'])
628
+ ],
629
+ // Выбор из списка с фильтром
630
+ 'status' => [
631
+ 'title' => LANG_STATUS,
632
+ 'width' => 100,
633
+ 'filter' => 'exact',
634
+ 'filter_select' => [
635
+ 'items' => function($name) {
636
+ return ['' => LANG_ALL, 0 => LANG_INACTIVE, 1 => LANG_ACTIVE];
637
+ }
638
+ ],
639
+ 'handler' => function ($value, $row) {
640
+ return $value ? '<span class="text-success">'.LANG_ACTIVE.'</span>'
641
+ : '<span class="text-muted">'.LANG_INACTIVE.'</span>';
642
+ }
643
+ ],
644
+ // Числовой диапазон фильтра
645
+ 'amount' => [
646
+ 'title' => LANG_AMOUNT,
647
+ 'filter' => 'range'
648
+ ],
649
+ // Кастомный обработчик для подсветки строки
650
+ 'priority' => [
651
+ 'title' => LANG_PRIORITY,
652
+ 'class_handler' => function($row) {
653
+ if ($row['priority'] > 5) { return 'bg-warning'; }
654
+ }
655
+ ]
656
+ ];
657
+
658
+ $actions = [
659
+ // Кнопка редактирования
660
+ [
661
+ 'title' => LANG_EDIT,
662
+ 'icon' => 'pen',
663
+ 'href' => href_to($controller->root_url, 'items', ['edit', '{id}'])
664
+ ],
665
+ // Кнопка копирования
666
+ [
667
+ 'title' => LANG_COPY,
668
+ 'icon' => 'copy',
669
+ 'href' => href_to($controller->root_url, 'items_add', ['{id}', 1])
670
+ ],
671
+ // Условная кнопка (только если handler вернул true)
672
+ [
673
+ 'title' => LANG_{NAME}_APPROVE,
674
+ 'icon' => 'check',
675
+ 'href' => href_to($controller->root_url, 'items', ['approve', '{id}']),
676
+ 'handler' => function ($item) {
677
+ return $item['is_pub'] == 0; // показать только для неопубликованных
678
+ }
679
+ ],
680
+ // Опасная кнопка с подтверждением
681
+ [
682
+ 'title' => LANG_DELETE,
683
+ 'class' => 'text-danger',
684
+ 'icon' => 'times-circle',
685
+ 'confirm' => LANG_DELETE_CONFIRM,
686
+ 'href' => href_to($controller->root_url, 'items', ['delete', '{id}'])
687
+ ]
688
+ ];
689
+
690
+ return [
691
+ 'options' => $options,
692
+ 'columns' => $columns,
693
+ 'actions' => $actions
694
+ ];
695
+ }`
696
+ },
697
+ {
698
+ path: "backend/forms/form_item.php",
699
+ required: true,
700
+ description: "Форма добавления/редактирования элемента в бэкенде. Класс наследует cmsForm. Метод init($do, ...) принимает 'add' или 'edit'.",
701
+ template: `<?php
702
+
703
+ class form{Name}Item extends cmsForm {
704
+
705
+ // init() получает параметры из $this->form_opts (установлены в экшене)
706
+ // $do = 'add' или 'edit'
707
+ public function init($do) {
708
+
709
+ return [
710
+ // Секция (fieldset)
711
+ 'basic' => [
712
+ 'title' => LANG_CP_BASIC, // заголовок секции
713
+ 'type' => 'fieldset',
714
+ 'childs' => [
715
+ new fieldString('title', [
716
+ 'title' => LANG_TITLE,
717
+ 'rules' => [
718
+ ['required'],
719
+ ['max_length', 255]
720
+ ]
721
+ ]),
722
+ new fieldHtml('text', [
723
+ 'title' => LANG_TEXT
724
+ ]),
725
+ new fieldCheckbox('is_pub', [
726
+ 'title' => LANG_IS_PUB,
727
+ 'default' => 1
728
+ ]),
729
+ // Список с динамическими значениями
730
+ new fieldList('category_id', [
731
+ 'title' => LANG_CATEGORY,
732
+ 'generator' => function () {
733
+ $model = cmsCore::getModel('content');
734
+ return [0 => LANG_NOT_SELECTED] + array_collection_to_list(
735
+ $model->getCategories('{name}'),
736
+ 'id', 'title'
737
+ );
738
+ }
739
+ ]),
740
+ // Группы пользователей
741
+ new fieldListGroups('groups', [
742
+ 'show_all' => true,
743
+ 'show_guests' => false
744
+ ]),
745
+ // Дата
746
+ new fieldDate('date_pub', [
747
+ 'title' => LANG_DATE_PUB,
748
+ 'default' => date('Y-m-d H:i:s')
749
+ ])
750
+ ]
751
+ ],
752
+ // Вторая секция
753
+ 'media' => [
754
+ 'title' => LANG_MEDIA,
755
+ 'type' => 'fieldset',
756
+ 'childs' => [
757
+ new fieldImage('image', [
758
+ 'title' => LANG_IMAGE,
759
+ 'max_width' => 1200,
760
+ 'max_size' => 5120
761
+ ])
762
+ ]
763
+ ]
764
+ ];
765
+ }
766
+
767
+ }`
768
+ },
769
+ {
770
+ path: "backend/forms/form_options.php",
771
+ required: false,
772
+ description: "Форма настроек дополнения. Используется автоматически при useDefaultOptionsAction = true.",
773
+ template: `<?php
774
+
775
+ class form{Name}Options extends cmsForm {
776
+
777
+ public function init() {
778
+ return [
779
+ 'basic' => [
780
+ 'title' => LANG_CP_BASIC,
781
+ 'type' => 'fieldset',
782
+ 'childs' => [
783
+ new fieldNumber('perpage', [
784
+ 'title' => LANG_LIST_LIMIT,
785
+ 'default' => 10,
786
+ 'rules' => [['required'], ['min', 1]]
787
+ ]),
788
+ new fieldCheckbox('use_moderation', [
789
+ 'title' => LANG_USE_MODERATION
790
+ ])
791
+ ]
792
+ ]
793
+ ];
794
+ }
795
+
796
+ }`
797
+ }
798
+ ]
799
+ },
800
+ with_hooks: {
801
+ type: "with_hooks",
802
+ description: "Дополнение, интегрирующееся с другими компонентами через хуки системы событий",
803
+ notes: [
804
+ "Каждый хук — отдельный файл в hooks/, имя файла = имя хука",
805
+ "Класс: on{AddonName}{HookName} (CamelCase имя хука)",
806
+ "Хук ДОЛЖЕН быть зарегистрирован в manifest.xml",
807
+ "Хук ОБЯЗАТЕЛЬНО должен возвращать $data (для filter-хуков)"
808
+ ],
809
+ files: [
810
+ {
811
+ path: "hooks/content_after_add_approve.php",
812
+ required: true,
813
+ description: "Пример хука. Файл = имя хука. Класс: on{Name}ContentAfterAddApprove.",
814
+ template: `<?php
815
+ // Файл: hooks/content_after_add_approve.php
816
+ // Хук срабатывает после одобрения нового материала контента
817
+
818
+ class on{Name}ContentAfterAddApprove extends cmsAction {
819
+
820
+ /**
821
+ * @param array $data Данные хука (зависят от конкретного хука)
822
+ * @return mixed Для filter-хуков ОБЯЗАТЕЛЬНО вернуть $data
823
+ */
824
+ public function run($data) {
825
+
826
+ $ctype_name = $data['ctype_name'];
827
+ $item = $data['item'];
828
+
829
+ // Доступны стандартные свойства cmsAction:
830
+ // $this->model — модель текущего дополнения ({name})
831
+ // $this->cms_user — текущий пользователь
832
+ // $this->request — объект запроса
833
+ // $this->cms_template — объект шаблона
834
+
835
+ // Для работы с другими контроллерами:
836
+ $activity = cmsCore::getController('activity');
837
+
838
+ // Ваша логика...
839
+
840
+ // ОБЯЗАТЕЛЬНО вернуть $data для filter-хуков!
841
+ return $data;
842
+ }
843
+
844
+ }`
845
+ },
846
+ {
847
+ path: "hooks/user_registered.php",
848
+ required: false,
849
+ description: "Пример action-хука (не возвращает данные)",
850
+ template: `<?php
851
+ // action-хук: вызывается как событие, возврат не обязателен
852
+
853
+ class on{Name}UserRegistered extends cmsAction {
854
+
855
+ public function run($data) {
856
+
857
+ $user_id = $data['user_id'];
858
+
859
+ // Действие при регистрации нового пользователя
860
+ $this->model->insert('{name}_user_data', [
861
+ 'user_id' => $user_id,
862
+ 'date_reg' => date('Y-m-d H:i:s')
863
+ ]);
864
+
865
+ return $data;
866
+ }
867
+
868
+ }`
869
+ },
870
+ {
871
+ path: "manifest.xml (с хуками)",
872
+ required: true,
873
+ description: "Хуки должны быть зарегистрированы в manifest.xml",
874
+ template: `<?xml version="1.0" encoding="utf-8"?>
875
+ <addon>
876
+ <name>{name}</name>
877
+ <hooks>
878
+ <!-- controller = имя дополнения, name = имя хука -->
879
+ <hook controller="{name}" name="content_after_add_approve" />
880
+ <hook controller="{name}" name="user_registered" />
881
+ </hooks>
882
+ </addon>`
883
+ }
884
+ ]
885
+ },
886
+ with_routes: {
887
+ type: "with_routes",
888
+ description: "Дополнение с кастомными маршрутами URL. routes.php — функция, возвращающая массив правил.",
889
+ notes: [
890
+ "Функция routes_{name}() должна возвращать массив маршрутов",
891
+ "Паттерн — регулярное выражение, числовые ключи — карта capture-групп к параметрам",
892
+ "action — имя экшена (файл actions/{action}.php, класс action{Name}{Action})"
893
+ ],
894
+ files: [
895
+ {
896
+ path: "routes.php",
897
+ required: false,
898
+ description: "Кастомные маршруты URL. Функция routes_{name}() возвращает массив правил.",
899
+ template: `<?php
900
+
901
+ function routes_{name}() {
902
+
903
+ return [
904
+ // /myaddon/{slug}.html → actions/view.php → run($slug)
905
+ [
906
+ 'pattern' => '/^([a-z0-9\\-_]+)\\.html$/',
907
+ 'action' => 'view',
908
+ 1 => 'slug'
909
+ ],
910
+ // /myaddon/cat/{category-slug}/{page}
911
+ [
912
+ 'pattern' => '/^cat\\/([a-z0-9\\-_]+)(\\/([0-9]+))?$/',
913
+ 'action' => 'category',
914
+ 1 => 'cat_slug',
915
+ 3 => 'page'
916
+ ],
917
+ // /myaddon/tag/{tag}
918
+ [
919
+ 'pattern' => '/^tag\\/([a-z0-9\\-_]+)$/',
920
+ 'action' => 'tag',
921
+ 1 => 'tag'
922
+ ],
923
+ // /myaddon/{ctype}/{item}.html — с фиксированным значением
924
+ [
925
+ 'pattern' => '/^([a-z0-9\\-_]+)\\/([a-z0-9\\-]+)\\.html$/',
926
+ 'action' => 'item',
927
+ 'ctype_name' => 'articles', // фиксированный параметр
928
+ 1 => 'category',
929
+ 2 => 'slug'
930
+ ]
931
+ ];
932
+
933
+ }`
934
+ }
935
+ ]
936
+ },
937
+ with_widget: {
938
+ type: "with_widget",
939
+ description: "Дополнение с виджетом для публичных страниц. Виджет: widget.php + options.form.php.",
940
+ notes: [
941
+ "Класс виджета: widget{Name}{WidgetName} extends cmsWidget",
942
+ "Метод run() возвращает массив переменных для шаблона или false (не выводить)",
943
+ "Шаблон виджета: /templates/{theme}/controllers/{name}/{widget_name}.tpl.php",
944
+ "Опции читаются через $this->getOption('key', default)",
945
+ "Для отключения кэша: public $is_cacheable = false",
946
+ "Доступны: $this->cms_user, $this->cms_template, $this->cms_config, $this->cms_core"
947
+ ],
948
+ files: [
949
+ {
950
+ path: "widgets/{widget_name}/widget.php",
951
+ required: true,
952
+ description: "Класс виджета. Наследует cmsWidget. Метод run() — основная логика.",
953
+ template: `<?php
954
+ /**
955
+ * Виджет списка элементов
956
+ * Шаблон: /templates/{theme}/controllers/{name}/list.tpl.php
957
+ * (имя шаблона = имя виджета по умолчанию)
958
+ */
959
+ class widget{Name}List extends cmsWidget {
960
+
961
+ // Отключить кэширование (для динамического контента):
962
+ // public $is_cacheable = false;
963
+
964
+ public function run() {
965
+
966
+ // Читать настройки виджета (устанавливаются в админке)
967
+ $limit = $this->getOption('limit', 5);
968
+ $is_pub = $this->getOption('is_pub', 1);
969
+ $dataset = $this->getOption('dataset', '');
970
+
971
+ // Загрузить контроллер для доступа к его модели
972
+ $controller = cmsCore::getController('{name}');
973
+
974
+ $items = $controller->model
975
+ ->filterEqual('is_pub', $is_pub)
976
+ ->orderBy('date_pub', 'desc')
977
+ ->limit($limit)
978
+ ->get('{name}_items');
979
+
980
+ // Если нет данных — не отображать виджет
981
+ if (!$items) {
982
+ return false;
983
+ }
984
+
985
+ // Возвращаем массив переменных для шаблона
986
+ return [
987
+ 'items' => $items,
988
+ 'limit' => $limit,
989
+ // Доступ к текущему пользователю:
990
+ // 'is_logged' => $this->cms_user->is_logged
991
+ ];
992
+ }
993
+
994
+ }`
995
+ },
996
+ {
997
+ path: "widgets/{widget_name}/options.form.php",
998
+ required: false,
999
+ description: "Форма настроек виджета в админке. Класс: formWidget{Name}{WidgetName}Options.",
1000
+ template: `<?php
1001
+ // ВАЖНО: класс формы = formWidget + PascalCase(controller) + PascalCase(widget_name) + Options
1002
+
1003
+ class formWidget{Name}ListOptions extends cmsForm {
1004
+
1005
+ public function init($options = false) {
1006
+ return [
1007
+ 'woptions' => [
1008
+ 'type' => 'fieldset',
1009
+ 'title' => LANG_OPTIONS,
1010
+ 'childs' => [
1011
+ new fieldNumber('options:limit', [
1012
+ 'title' => LANG_LIST_LIMIT,
1013
+ 'default' => 5,
1014
+ 'options' => ['is_abs' => true]
1015
+ ]),
1016
+ new fieldList('options:is_pub', [
1017
+ 'title' => 'Показывать',
1018
+ 'default' => 1,
1019
+ 'items' => [1 => 'Опубликованные', 0 => 'Все']
1020
+ ]),
1021
+ // Динамический список категорий
1022
+ new fieldList('options:category_id', [
1023
+ 'title' => LANG_CATEGORY,
1024
+ 'generator' => function () {
1025
+ $model = cmsCore::getModel('content');
1026
+ return [0 => LANG_ALL_CATEGORIES] + array_collection_to_list(
1027
+ $model->getCategories('{name}'), 'id', 'title'
1028
+ );
1029
+ }
1030
+ ])
1031
+ ]
1032
+ ]
1033
+ ];
1034
+ }
1035
+
1036
+ }`
1037
+ }
1038
+ ]
1039
+ }
1040
+ };
1041
+ // ─────────────────────────────────────────────────────────────────────────────
1042
+ // СТРУКТУРА ШАБЛОНА (ТЕМЫ)
1043
+ // ─────────────────────────────────────────────────────────────────────────────
1044
+ exports.templateStructure = {
1045
+ description: "Структура шаблона (темы) для InstantCMS. Все шаблоны в /templates/{theme}/. Бэкенд-шаблоны контроллеров: /templates/{theme}/controllers/{name}/backend/{action}.tpl.php. Тема admincoreui предоставляет только layout-оболочку (navbar/sidebar), а НЕ шаблоны контента контроллеров.",
1046
+ install_note: "Шаблоны находятся в /templates/, а НЕ в /system/templates/. Бэкенд-шаблоны в подпапке backend/ внутри папки контроллера frontend-темы.",
1047
+ directory_layout: `
1048
+ /templates/{theme_name}/
1049
+ ├── manifest.php ← ОБЯЗАТЕЛЬНО: метаданные шаблона
1050
+ ├── main.tpl.php ← ОБЯЗАТЕЛЬНО: главный макет HTML
1051
+ ├── options.form.php ← форма настроек шаблона в админке
1052
+ ├── options.css.php ← динамический CSS из настроек
1053
+ ├── scheme.php ← цветовые схемы
1054
+ ├── scss/ ← SCSS-исходники (style_middleware => 'scss')
1055
+ ├── css/ ← скомпилированные стили
1056
+ │ ├── main.css
1057
+ │ └── ...
1058
+ ├── js/ ← скрипты
1059
+ │ ├── main.js
1060
+ │ └── ...
1061
+ ├── images/ ← изображения шаблона
1062
+ ├── assets/ ← прочие статические ресурсы
1063
+ ├── layout_childs/ ← фрагменты для динамического layout (is_dynamic_layout=true)
1064
+ │ └── main_scheme.tpl.php ← схема рядов/колонок Bootstrap; вызывается через renderLayoutChild()
1065
+ ├── widgets/ ← глобальные шаблоны виджетов
1066
+ │ ├── wrapper.tpl.php ← стандартная обёртка виджета
1067
+ │ ├── wrapper_tabbed.tpl.php
1068
+ │ ├── wrapper_plain.tpl.php
1069
+ │ └── {widget_type}/
1070
+ │ └── {widget_name}.tpl.php
1071
+ └── controllers/ ← шаблоны контроллеров (переопределяют системные)
1072
+ └── {controller_name}/
1073
+ ├── index.tpl.php ← фронтенд: экшен index
1074
+ ├── view.tpl.php ← фронтенд: экшен view
1075
+ ├── ... ← прочие фронтенд-экшены
1076
+ ├── backend/ ← БЭКЕНД-шаблоны (НЕ в admincoreui/!)
1077
+ │ ├── index.tpl.php ← бэкенд: дашборд-страница
1078
+ │ ├── items.tpl.php ← бэкенд: список (генерируется listgrid)
1079
+ │ ├── items_add.tpl.php ← бэкенд: форма добавления/редактирования
1080
+ │ └── ...
1081
+ └── widgets/ ← шаблоны виджетов этого контроллера
1082
+ └── {widget_name}/
1083
+ └── {widget_name}.tpl.php
1084
+ `,
1085
+ widget_positions: `
1086
+ Стандартные позиции виджетов (вызов в main.tpl.php):
1087
+ <?= $this->widgets('header') ?> ← шапка
1088
+ <?= $this->widgets('top') ?> ← навигация
1089
+ <?= $this->widgets('left-top') ?> ← левая колонка верх
1090
+ <?= $this->widgets('left-bottom') ?> ← левая колонка низ
1091
+ <?= $this->widgets('right-top') ?> ← правая колонка верх
1092
+ <?= $this->widgets('right-center') ?> ← правая колонка середина
1093
+ <?= $this->widgets('right-bottom') ?> ← правая колонка низ
1094
+ <?= $this->widgets('footer') ?> ← подвал
1095
+ <?= $this->body() ?> ← основной контент страницы
1096
+
1097
+ Проверка наличия виджетов:
1098
+ <?php if ($this->hasWidgetsOn('right-top')): ?>
1099
+ `,
1100
+ template_variables: `
1101
+ Переменные доступные в .tpl.php шаблонах:
1102
+ $this->title() — заголовок страницы (метод с (), не свойство!)
1103
+ $this->body() — основной контент (HTML)
1104
+ $this->widgets($pos) — вывод виджетов позиции
1105
+ $this->widgetsInHtml($pos, $w) — виджеты позиции в HTML-обёртке
1106
+ $this->hasWidgetsOn($pos) — проверка наличия виджетов на позиции
1107
+ $this->breadcrumbs() — хлебные крошки
1108
+ $this->head(true, ...) — теги <head>; первый параметр = include_css_js
1109
+ $this->bottom() — скрипты перед </body>
1110
+ $this->linkCSS('css/main.css') — подключение CSS файла темы
1111
+ $this->linkJS('js/main.js') — подключение JS файла темы
1112
+ $this->addMainTplCSSName(...) — добавить CSS класс к <html>/<body>
1113
+ $this->addMainTplJSName(...) — добавить JS класс
1114
+ $this->renderLayoutChild($name, $vars) — рендер фрагмента из layout_childs/
1115
+ $this->href_to($action, $params) — формирование URL к экшену контроллера
1116
+ $config — объект конфигурации сайта
1117
+ $cms_user — текущий пользователь (cmsUser)
1118
+ cmsUser::isAdmin() — проверка на администратора
1119
+ cmsUser::isLogged() — проверка авторизации
1120
+ $device_type — 'mobile' или 'desktop'
1121
+ LANG_CODE — код активного языка (ru, en, ...)
1122
+ `,
1123
+ inheritance: {
1124
+ description: "Система наследования шаблонов через manifest.php. Тема может наследовать файлы из других тем.",
1125
+ chain_resolution: "Алгоритм: setInheritNames() строит [default, ...inherited, current] → reverses → current проверяется ПЕРВЫМ. Первое найденное совпадение выигрывает.",
1126
+ admincoreui_role: "admincoreui — ТОЛЬКО layout-оболочка для бэкенда (admin.tpl.php содержит navbar + sidebar Bootstrap 4 CoreUI). Шаблоны контента контроллеров находятся в frontend-теме (modern/controllers/{name}/backend/).",
1127
+ manifest_inherit_example: `<?php
1128
+ // templates/modern/manifest.php
1129
+ return [
1130
+ 'inherit' => ['admincoreui'], // наследовать файлы из admincoreui
1131
+ 'title' => 'Modern',
1132
+ 'properties' => [
1133
+ 'vendor' => 'bootstrap4',
1134
+ 'style_middleware' => 'scss', // SCSS компиляция
1135
+ 'has_options' => true,
1136
+ 'is_dynamic_layout' => true, // использует layout_childs/
1137
+ 'is_backend' => false, // НЕ backend-тема
1138
+ 'is_frontend' => true,
1139
+ 'html_attr' => ['class' => 'min-vh-100']
1140
+ ]
1141
+ ];
1142
+
1143
+ // templates/admincoreui/manifest.php
1144
+ return [
1145
+ 'inherit' => ['modern'], // наследовать из modern (взаимное наследование!)
1146
+ 'title' => 'CoreUI',
1147
+ 'properties' => [
1148
+ 'has_options' => false,
1149
+ 'is_dynamic_layout' => false,
1150
+ 'is_backend' => true, // backend-тема (layout для админки)
1151
+ 'is_frontend' => false
1152
+ ]
1153
+ ];`,
1154
+ dynamic_layout: `// is_dynamic_layout = true (в manifest.php)
1155
+ // В main.tpl.php вместо явных вызовов $this->widgets('left-top') и т.д.:
1156
+ $this->renderLayoutChild('scheme', ['rows' => $rows]);
1157
+ // Шаблон layout_childs/main_scheme.tpl.php получает $rows (массив рядов/колонок)
1158
+ // и итерирует их, выводя виджеты по позициям Bootstrap-сеткой`,
1159
+ inheritance_chain_example: [
1160
+ "modern: 'inherit' => ['admincoreui'] — frontend-тема, наследует layout из admincoreui",
1161
+ "admincoreui: 'inherit' => ['modern'] — backend layout, наследует компоненты из modern",
1162
+ "Поиск файла шаблона: modern → admincoreui → default (current FIRST)"
1163
+ ]
1164
+ },
1165
+ required_files: [
1166
+ {
1167
+ path: "manifest.php",
1168
+ description: "Метаданные шаблона. Возвращает массив с title, author, properties.",
1169
+ template: `<?php
1170
+ return [
1171
+ // 'inherit' => ['admincoreui'], // наследование: modern наследует из admincoreui
1172
+ 'title' => 'My Theme',
1173
+ 'author' => [
1174
+ 'name' => 'Author Name',
1175
+ 'url' => 'https://example.com',
1176
+ 'help' => 'https://docs.example.com'
1177
+ ],
1178
+ 'properties' => [
1179
+ 'has_options' => true, // есть форма настроек шаблона
1180
+ 'has_profile_themes_support' => false, // поддержка профильных тем
1181
+ 'has_profile_themes_options' => false,
1182
+ 'is_dynamic_layout' => false, // true = использует layout_childs/ + renderLayoutChild()
1183
+ 'is_backend' => false, // true = шаблон для admin-layout (как admincoreui)
1184
+ 'is_frontend' => true, // шаблон доступен для фронтенда
1185
+ 'vendor' => 'bootstrap4', // CSS-фреймворк
1186
+ 'style_middleware' => 'scss' // 'scss' если используется SCSS компиляция
1187
+ ]
1188
+ ];`
1189
+ },
1190
+ {
1191
+ path: "main.tpl.php",
1192
+ description: "Главный макет. HTML-скелет. Вызывает позиции виджетов и основной контент.",
1193
+ template: `<!DOCTYPE html>
1194
+ <html lang="<?= LANG_CODE ?>">
1195
+ <head>
1196
+ <meta charset="utf-8">
1197
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1198
+ <title><?= $this->title() ?></title>
1199
+ <?= $this->head() ?>
1200
+ <?= $this->linkCSS('css/theme-layout.css') ?>
1201
+ <?= $this->linkCSS('css/theme-gui.css') ?>
1202
+ </head>
1203
+ <body>
1204
+ <header>
1205
+ <?= $this->widgets('header') ?>
1206
+ <?= $this->widgets('top') ?>
1207
+ </header>
1208
+
1209
+ <?= $this->breadcrumbs() ?>
1210
+
1211
+ <div class="container">
1212
+ <?php if ($this->hasWidgetsOn('left-top') || $this->hasWidgetsOn('left-bottom')): ?>
1213
+ <aside class="sidebar-left">
1214
+ <?= $this->widgets('left-top') ?>
1215
+ <?= $this->widgets('left-bottom') ?>
1216
+ </aside>
1217
+ <?php endif ?>
1218
+
1219
+ <main>
1220
+ <?= $this->body() ?>
1221
+ </main>
1222
+
1223
+ <?php if ($this->hasWidgetsOn('right-top')): ?>
1224
+ <aside class="sidebar-right">
1225
+ <?= $this->widgets('right-top') ?>
1226
+ <?= $this->widgets('right-center') ?>
1227
+ <?= $this->widgets('right-bottom') ?>
1228
+ </aside>
1229
+ <?php endif ?>
1230
+ </div>
1231
+
1232
+ <footer>
1233
+ <?= $this->widgets('footer') ?>
1234
+ </footer>
1235
+
1236
+ <?= $this->bottom() ?>
1237
+ </body>
1238
+ </html>`
1239
+ }
1240
+ ],
1241
+ optional_files: [
1242
+ { path: "options.form.php", description: "Форма настроек шаблона в админке" },
1243
+ { path: "options.css.php", description: "CSS-переменные из настроек шаблона" },
1244
+ { path: "scheme.php", description: "Описание цветовых схем" },
1245
+ { path: "css/main.css", description: "Основные стили" },
1246
+ { path: "js/main.js", description: "Основные скрипты" },
1247
+ { path: "images/", description: "Изображения шаблона" }
1248
+ ],
1249
+ controller_templates: {
1250
+ description: "Шаблоны для конкретных контроллеров. Переопределяют системные шаблоны. Фронтенд и бэкенд — в одной папке frontend-темы.",
1251
+ frontend_path: "/templates/{theme}/controllers/{name}/{action}.tpl.php",
1252
+ backend_path: "/templates/{theme}/controllers/{name}/backend/{action}.tpl.php",
1253
+ widget_path: "/templates/{theme}/controllers/{name}/widgets/{widget_name}/{widget_name}.tpl.php",
1254
+ critical_note: "ВАЖНО: бэкенд-шаблоны в подпапке backend/ внутри папки frontend-темы (modern/), НЕ в папке admincoreui/!",
1255
+ examples: [
1256
+ "templates/modern/controllers/content/default_list.tpl.php — список материалов (фронтенд)",
1257
+ "templates/modern/controllers/content/default_item.tpl.php — просмотр материала (фронтенд)",
1258
+ "templates/modern/controllers/users/profile.tpl.php — профиль пользователя (фронтенд)",
1259
+ "templates/modern/controllers/{name}/index.tpl.php — главная страница дополнения (фронтенд)",
1260
+ "templates/modern/controllers/{name}/backend/index.tpl.php — дашборд бэкенда",
1261
+ "templates/modern/controllers/{name}/backend/items.tpl.php — список (бэкенд)",
1262
+ "templates/modern/controllers/{name}/widgets/{wname}/{wname}.tpl.php — виджет"
1263
+ ],
1264
+ note: "Шаблоны в папке темы ПЕРЕОПРЕДЕЛЯЮТ системные шаблоны из /system/controllers/. Если файл не найден в теме — ищется в inherited темах, затем в default."
1265
+ }
1266
+ };
1267
+ // ─────────────────────────────────────────────────────────────────────────────
1268
+ // ТИПЫ ПОЛЕЙ ФОРМ
1269
+ // ─────────────────────────────────────────────────────────────────────────────
1270
+ exports.fieldTypes = {
1271
+ fieldString: {
1272
+ description: "Однострочное текстовое поле (input text)",
1273
+ example: `new fieldString('title', [
1274
+ 'title' => LANG_TITLE,
1275
+ 'rules' => [['required'], ['max_length', 255]],
1276
+ 'hint' => 'Подсказка под полем'
1277
+ ])`
1278
+ },
1279
+ fieldText: {
1280
+ description: "Многострочное текстовое поле (textarea)",
1281
+ example: `new fieldText('description', [
1282
+ 'title' => LANG_DESCRIPTION,
1283
+ 'rows' => 5
1284
+ ])`
1285
+ },
1286
+ fieldHtml: {
1287
+ description: "HTML редактор (WYSIWYG/TinyMCE)",
1288
+ example: `new fieldHtml('content', [
1289
+ 'title' => LANG_CONTENT,
1290
+ 'rules' => [['required']]
1291
+ ])`
1292
+ },
1293
+ fieldNumber: {
1294
+ description: "Числовое поле с опциями is_abs (только положительные), is_ceil (целые), save_zero",
1295
+ example: `new fieldNumber('amount', [
1296
+ 'title' => LANG_AMOUNT,
1297
+ 'default' => 0,
1298
+ 'options' => [
1299
+ 'is_abs' => true, // только положительные
1300
+ 'is_ceil' => true, // только целые
1301
+ 'save_zero' => false // не сохранять ноль
1302
+ ],
1303
+ 'rules' => [['min', 0], ['max', 999999]]
1304
+ ])`
1305
+ },
1306
+ fieldList: {
1307
+ description: "Выпадающий список (select). Поддерживает generator для динамического наполнения.",
1308
+ example: `new fieldList('status', [
1309
+ 'title' => LANG_STATUS,
1310
+ 'default' => 1,
1311
+ 'items' => [0 => LANG_INACTIVE, 1 => LANG_ACTIVE],
1312
+ // Или динамически:
1313
+ // 'generator' => function() { return [...]; }
1314
+ ])`
1315
+ },
1316
+ fieldListMultiple: {
1317
+ description: "Множественный выбор (checkbox list или multiselect)",
1318
+ example: `new fieldListMultiple('categories', [
1319
+ 'title' => LANG_CATEGORIES,
1320
+ 'generator' => function () {
1321
+ return array_collection_to_list(
1322
+ cmsCore::getModel('content')->getCategories('articles'),
1323
+ 'id', 'title'
1324
+ );
1325
+ }
1326
+ ])`
1327
+ },
1328
+ fieldListGroups: {
1329
+ description: "Выбор групп пользователей. Специализированный вариант fieldList.",
1330
+ example: `new fieldListGroups('groups', [
1331
+ 'hint' => 'Группы с доступом к плану',
1332
+ 'show_all' => false, // не показывать "Все"
1333
+ 'show_guests' => false // не показывать гостей
1334
+ ])`
1335
+ },
1336
+ fieldCheckbox: {
1337
+ description: "Чекбокс (булево поле). default = 0 или 1.",
1338
+ example: `new fieldCheckbox('is_enabled', [
1339
+ 'title' => LANG_IS_ENABLED,
1340
+ 'label' => 'Включить эту функцию',
1341
+ 'default' => 1
1342
+ ])`
1343
+ },
1344
+ fieldImage: {
1345
+ description: "Загрузка одного изображения с превью",
1346
+ example: `new fieldImage('photo', [
1347
+ 'title' => LANG_PHOTO,
1348
+ 'max_width' => 1200,
1349
+ 'max_height' => 900,
1350
+ 'max_size' => 5120 // KB
1351
+ ])`
1352
+ },
1353
+ fieldFile: {
1354
+ description: "Загрузка произвольного файла",
1355
+ example: `new fieldFile('attachment', [
1356
+ 'title' => LANG_ATTACHMENT,
1357
+ 'max_size' => 10240
1358
+ ])`
1359
+ },
1360
+ fieldDate: {
1361
+ description: "Поле выбора даты и времени (datepicker)",
1362
+ example: `new fieldDate('date_pub', [
1363
+ 'title' => LANG_DATE_PUB,
1364
+ 'default' => date('Y-m-d H:i:s')
1365
+ ])`
1366
+ },
1367
+ fieldUrl: {
1368
+ description: "Поле для URL с валидацией",
1369
+ example: `new fieldUrl('website', [
1370
+ 'title' => LANG_WEBSITE,
1371
+ 'rules' => [['url']]
1372
+ ])`
1373
+ },
1374
+ fieldHidden: {
1375
+ description: "Скрытое поле (не отображается пользователю)",
1376
+ example: `new fieldHidden('user_id', [
1377
+ 'default' => $this->cms_user->id
1378
+ ])`
1379
+ },
1380
+ fieldCategory: {
1381
+ description: "Выбор категории типа контента",
1382
+ example: `new fieldCategory('category_id', [
1383
+ 'title' => LANG_CATEGORY,
1384
+ 'ctype_name' => 'articles'
1385
+ ])`
1386
+ },
1387
+ fieldFieldsgroup: {
1388
+ description: "Повторяющаяся группа полей (динамический список вложенных полей). Используется для сложных структур данных.",
1389
+ example: `new fieldFieldsgroup('prices', [
1390
+ 'add_title' => LANG_PRICES_ADD,
1391
+ 'is_sortable' => true,
1392
+ 'childs' => [
1393
+ new fieldNumber('amount', [
1394
+ 'title' => LANG_AMOUNT,
1395
+ 'rules' => [['required']]
1396
+ ]),
1397
+ new fieldList('period', [
1398
+ 'title' => LANG_PERIOD,
1399
+ 'items' => ['DAY' => LANG_DAY1, 'MONTH' => LANG_MONTH1]
1400
+ ])
1401
+ ]
1402
+ ])`
1403
+ }
1404
+ };