@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.
- package/LICENSE +21 -0
- package/README.md +308 -0
- package/dist/__tests__/artifact-tool.test.js +196 -0
- package/dist/__tests__/db-tool.test.js +152 -0
- package/dist/__tests__/define-tool.test.js +106 -0
- package/dist/__tests__/find-tool.test.js +185 -0
- package/dist/__tests__/get-component-api.test.js +144 -0
- package/dist/__tests__/hardening.test.js +73 -0
- package/dist/__tests__/hooks-tool.test.js +173 -0
- package/dist/__tests__/mcp-integration-extra.test.js +166 -0
- package/dist/__tests__/mcp-integration.test.js +53 -0
- package/dist/__tests__/pagination.test.js +114 -0
- package/dist/__tests__/performance-baseline.test.js +56 -0
- package/dist/__tests__/project-patch.test.js +144 -0
- package/dist/__tests__/project-source.test.js +44 -0
- package/dist/__tests__/project-workflows-extra.test.js +165 -0
- package/dist/__tests__/project-workflows.test.js +51 -0
- package/dist/__tests__/scaffold-addon-roundtrip.test.js +287 -0
- package/dist/__tests__/serialization.test.js +170 -0
- package/dist/__tests__/source-knowledge-parsers.test.js +42 -0
- package/dist/__tests__/template-development.test.js +78 -0
- package/dist/__tests__/template-productivity.test.js +87 -0
- package/dist/__tests__/tools.test.js +2143 -0
- package/dist/data/components.js +2818 -0
- package/dist/data/controllers-map.js +7571 -0
- package/dist/data/core-api.js +9356 -0
- package/dist/data/database-schema.js +4555 -0
- package/dist/data/events-map.js +1339 -0
- package/dist/data/fields-map.js +3692 -0
- package/dist/data/hooks.js +2257 -0
- package/dist/data/js-api.js +123 -0
- package/dist/data/libs-api.js +264 -0
- package/dist/data/routes-map.js +203 -0
- package/dist/data/schemas-validation.js +255 -0
- package/dist/data/schemas.js +1404 -0
- package/dist/data/traits-map.js +1004 -0
- package/dist/data/version-profiles.js +41 -0
- package/dist/data/widgets-map.js +131 -0
- package/dist/data/wysiwyg-map.js +486 -0
- package/dist/generated/components-source.js +14748 -0
- package/dist/generated/hooks-source.js +3399 -0
- package/dist/generated/knowledge-meta.js +101 -0
- package/dist/index.js +13 -0
- package/dist/registry/database-tools.js +112 -0
- package/dist/registry/extension-tools.js +473 -0
- package/dist/registry/generator-tools.js +506 -0
- package/dist/registry/knowledge-tools.js +441 -0
- package/dist/registry/language-tools.js +99 -0
- package/dist/registry/meta-tools.js +152 -0
- package/dist/registry/project-tools.js +48 -0
- package/dist/registry/resources.js +98 -0
- package/dist/registry/source-tools.js +211 -0
- package/dist/registry/template-development-tools.js +79 -0
- package/dist/server.js +32 -0
- package/dist/tools/addon-tool.js +1437 -0
- package/dist/tools/admin-partial-tool.js +498 -0
- package/dist/tools/api-tool.js +294 -0
- package/dist/tools/artifact-tool.js +95 -0
- package/dist/tools/cache-tool.js +360 -0
- package/dist/tools/component-tool.js +415 -0
- package/dist/tools/controllers-tool.js +125 -0
- package/dist/tools/cron-tool.js +197 -0
- package/dist/tools/crud-tool.js +659 -0
- package/dist/tools/db-tool.js +198 -0
- package/dist/tools/email-tool.js +222 -0
- package/dist/tools/external-api-tool.js +596 -0
- package/dist/tools/filter-tool.js +341 -0
- package/dist/tools/form-tool.js +275 -0
- package/dist/tools/grid-tool.js +189 -0
- package/dist/tools/hooks-tool.js +104 -0
- package/dist/tools/import-export-tool.js +548 -0
- package/dist/tools/lang-tool.js +251 -0
- package/dist/tools/layout-override-tool.js +125 -0
- package/dist/tools/layout-tool.js +548 -0
- package/dist/tools/maria-tool.js +127 -0
- package/dist/tools/mariadb.js +201 -0
- package/dist/tools/migration-tool.js +329 -0
- package/dist/tools/oauth-tool.js +520 -0
- package/dist/tools/parser/components-parser.js +76 -0
- package/dist/tools/parser/controllers-parser.js +313 -0
- package/dist/tools/parser/core-parser.js +294 -0
- package/dist/tools/parser/coverage-generator.js +424 -0
- package/dist/tools/parser/events-parser.js +127 -0
- package/dist/tools/parser/fields-parser.js +382 -0
- package/dist/tools/parser/hooks-parser.js +106 -0
- package/dist/tools/parser/sql-parser.js +197 -0
- package/dist/tools/parser/traits-parser.js +161 -0
- package/dist/tools/parser/widgets-parser.js +150 -0
- package/dist/tools/permission-tool.js +348 -0
- package/dist/tools/project-patch-tool.js +73 -0
- package/dist/tools/project-source-tool.js +188 -0
- package/dist/tools/project-workflow-tool.js +186 -0
- package/dist/tools/requirement-tool.js +212 -0
- package/dist/tools/scaffold-tool.js +815 -0
- package/dist/tools/seo-tool.js +412 -0
- package/dist/tools/source-tool.js +155 -0
- package/dist/tools/template-development-tool.js +271 -0
- package/dist/tools/template-overrides-tool.js +294 -0
- package/dist/tools/template-productivity-tool.js +319 -0
- package/dist/tools/template-tool.js +665 -0
- package/dist/tools/test-tool.js +183 -0
- package/dist/tools/webhook-tool.js +427 -0
- package/dist/tools/widget-tool.js +331 -0
- package/dist/tools/wysiwyg-tool.js +137 -0
- package/dist/types/scaffold.js +68 -0
- package/dist/utils/define-tool.js +37 -0
- package/dist/utils/find-tool.js +97 -0
- package/dist/utils/mcp-result.js +18 -0
- package/dist/utils/pagination.js +27 -0
- package/dist/utils/serialization.js +27 -0
- 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
|
+
};
|