id = $id;
$this->setController($controller, $action);
}
/**
* Инициализация
*/
public function init()
{
parent::init();
$this->setTemplateDir(
$this->app->corePath('tpl/admin/' . $this->app->adminTheme() . '/form')
);
$this->wrapper()->icon(false);
$this->setTemplateName('layout');
$this->detectAjaxInit();
$this->trigger('init');
}
/**
* Fire form event
* @param string $event
* @param array $data
* @return static
*/
public function trigger(string $event, array $data = [])
{
$this->app->hook($this->id . '.admin.form.' . $event, $this, $data);
return $this;
}
/**
* Получаем ID формы
* @return int
*/
public function id()
{
return $this->id;
}
/**
* Установить ID записи
* @param int|string $id
* @return static
*/
public function setRecordID($id)
{
$this->recordID = $id;
if ($this->_onRecordID && is_callable($this->_onRecordID)) {
$this->recordID = call_user_func($this->_onRecordID, $id);
}
return $this;
}
/**
* Получаем ID записи
* @return string
*/
public function recordID()
{
return $this->recordID;
}
/**
* Установка обработчика события установки ID записи
* @param callable $callback функция обработчика, аргументы ($recordID) $recordID - ID записи,
* функция должна вернуть ID записи
* @return static
*/
public function onRecordID(callable $callback)
{
$this->_onRecordID = $callback;
return $this;
}
/**
* Указание модели для работы с данными формы
* @param Model $model
* @param array $opts [
* 'with' => relations
* 'tag' => string
* 'beforeSave' => callable ($data, $id) return array data to save
* 'afterLoad' => callable ($data, $id, $fields) return array data
* ]
* @return static
*/
public function useModel(Model $model, array $opts = [])
{
$opts = $this->defaults($opts, [
'with' => [],
'tag' => false,
'beforeSave' => null,
'afterLoad' => null,
]);
$this->model = $model;
if (is_null($this->_onLoad)) {
$this->onLoad(function($id, $fields) use ( & $opts) {
$model = $this->getModel();
if (! $opts['tag']) {
$opts['tags'] = 'admin.form.load.'.$this->getControllerName().'.'.$this->getControllerAction();
}
$opts['form'] = $this;
$data = $model->onAdminFormLoad($id, $opts);
if (is_callable($opts['afterLoad'])) {
$data = call_user_func($opts['afterLoad'], $data, $id, $fields);
}
return $data;
});
}
if (is_null($this->_onSave)) {
$this->onSave(function($id, $data) use ( & $opts) {
$model = $this->getModel();
if (is_callable($opts['beforeSave'])) {
$data = call_user_func($opts['beforeSave'], $data, $id);
}
if ($id) {
return $model->find($id)->fill($data)->save() ? $id : false;
} else {
$model->fill($data)->save();
return $model->getKey();
}
}, false);
}
return $this;
}
/**
* Получение модели для работы с данными формы
* @return Model|null
*/
public function getModel()
{
return $this->model;
}
/**
* Установка обработчика события получения данных о записи
* @param callable $callback функция обработчика, аргументы ($recordID, array $fields) $recordID - ID записи, $fields - массив со списком необходимых полей
* функция должна вернуть массив с данными
* @return static
*/
public function onLoad(callable $callback)
{
$this->_onLoad = $callback;
$this->checkAjax();
return $this;
}
public function issetLoad()
{
return is_callable($this->_onLoad);
}
/**
* Установка обработчика события сохранения данных о записи
* @param callable $callback функция обработчика, аргументы ($recordID, $data) $recordID - ID записи, $data - массив данных в формате [имяполя => значение]
* если $recordID == 0 - добавление новой записи, должна вернуть ID добавленной записи
* функция должна вернуть результат сохранения
* @param bool $toJSON преобразовывать данные типа TYPE_ARRAY к JSON строке
* @return static
*/
public function onSave(callable $callback, $toJSON = true)
{
$this->_onSave = $callback;
$this->toJSON = $toJSON;
$this->checkAjax();
return $this;
}
/**
* @return bool
*/
public function issetSave()
{
return is_callable($this->_onSave);
}
/**
* Приводить или нет сохраняемые массивы к JSON строкам
* @return bool
*/
public function _toJSON()
{
return $this->toJSON;
}
/**
* Проверка возможности запуска обработчика ajax запросов
* @return void
*/
protected function checkAjax()
{
if ($this->issetSave() && $this->issetLoad()) {
bff::hook('admin.form.init.'.$this->getControllerName().'.'.$this->getControllerAction(), $this);
$this->ajax();
}
}
/**
* Построение контента формы
* @param array $data
* @return string HTML
*/
public function content($data = [])
{
$data = $this->defaults($data, [
'form' => false,
'settingsForm' => false,
]);
$priority = [];
$i = 0;
foreach ($this->tabs as $k => & $v) {
$i++;
$priority[$k] = $v['priority'] ? $v['priority'] : $i;
$v['value'] = $this->value($k, false);
} unset($v);
asort($priority, SORT_NUMERIC);
$data['priority'] = $priority;
$data['tabs'] = & $this->tabs;
$data['fields'] = & $this->fields;
$data['cntTabs'] = 0;
$tab = '';
$ftab = $this->input->getpost('ftab', TYPE_STR);
if (! empty($ftab)) {
foreach ($this->tabs as $v) {
if ($v['name'] == $ftab) {
$tab = $v['name'];
break;
}
}
} else {
foreach ($this->tabs as $v) {
if ($v['default']) {
$tab = $v['name'];
break;
}
}
}
if (empty($tab)) {
$t = array_keys($priority);
$t = reset($t);
if (isset($this->tabs[$t])){
$tab = $this->tabs[$t]['name'];
}
}
$data['tab'] = $tab;
foreach ($this->tabs as & $v) {
$v['counter'] = static::obCallableAuto($v['counter']);
$v['style'] = static::obCallableAuto($v['style']);
} unset($v);
$data['tabsBlock'] = $this->render($data, 'tabs');
$priority = [];
foreach ($this->fields as $k => & $v) {
$priority[$k] = $v['priority'];
$v['value'] = $this->value($k, false);
} unset($v);
asort($priority, SORT_NUMERIC);
$data['priority'] = $priority;
$content = $this->render($data, 'content');
static::obCallable($content, $this->_contentWrapper, function($callable) use ( & $content) {
return call_user_func($callable, $content);
});
if ($data['form']) {
$priority = [];
if (! is_array($this->buttons)) {
$this->buttons = [];
}
foreach ($this->buttons as $k => $v) {
$priority[$k] = $v['priority'];
}
asort($priority, SORT_NUMERIC);
$data['priority'] = $priority;
$data['buttons'] = $this->buttons;
$data['iframe'] = $this->iframe;
$data['buttons'] = $this->render($data, 'buttons');
}
static::obCallable($this->_contentRight['html'], $this->_contentRight, function($callable) {
return call_user_func($callable, $this);
});
static::obCallable($this->_contentTop['html'], $this->_contentTop, function($callable) {
return call_user_func($callable, $this);
});
static::obCallable($this->_contentBottom['html'], $this->_contentBottom, function($callable) {
return call_user_func($callable, $this);
});
$layout = [
'form' => $data['form'],
'content' => $content,
'tabs' => $data['tabsBlock'],
'buttons' => isset($data['buttons']) ? $data['buttons'] : '',
'alerts' => $this->_alerts,
'right' => $this->_contentRight,
'top' => $this->_contentTop,
'bottom' => $this->_contentBottom,
'iframe' => $this->iframe,
];
return $this->render($layout);
}
/**
* Построение формы
* @param array $data доп. данные шаблона
* @return string HTML
* @throws AdminFormsException
*/
public function view(array & $data = [])
{
$this->ajax();
if (isset($data['recordID'])) {
$this->setRecordID($data['recordID']);
$this->applyWhen();
$this->load();
if ($this->buttonsDefault()) {
$this->buttonSubmit('', null, ['group' => true, 'priority' => 10]);
if (! empty($data['recordID'])) {
$this->buttonSubmitReturn('', ['group' => true, 'priority' => 11]);
}
$this->buttonCancel('', ['priority' => 100]);
}
} else {
$this->applyWhen();
}
$html = $this->content(['form' => true]);
$this->ajaxInitResponse($html);
return $html;
}
/**
* Валидация данных формы
* @param array $post данные для для сабмита, [] - взять из $_POST
* @return array данные для созранения
*/
public function _validate(array $post = [])
{
$isPost = empty($post);
$result = [];
foreach ($this->fields as $v) {
/** @var Input $field */
$field = $v['field'];
$name = $field->name();
$clean = $field->clean();
if ($isPost) {
if ($clean != TYPE_BOOL && ! isset($_POST[$name])) {
continue;
}
$value = $this->input->post($name, $clean);
} else {
if ($clean != TYPE_BOOL && ! isset($post[$name])) {
continue;
}
$value = $this->input->clean($post[$name], $clean);
}
if (! $field->validate(['value' => & $value, 'result' => & $result])) {
continue;
}
$result[$name] = $value;
}
return $result;
}
/**
* Дополнительная валидация формы после сохранения (перенос удаление файлов и т.д.)
* @param bool $isInserted флаг добавления записи
* @param array $saved @ref сохраненные данные
* @return array|bool
*/
public function _validateAfter($isInserted = false, array & $saved = [])
{
$result = [];
foreach ($this->fields as $v) {
/** @var Input $field */
$field = $v['field'];
$field->validateAfter($isInserted, ['saved' => & $saved, 'result' => & $result]);
}
return $result;
}
/**
* Вернуть значение для поля
* @param string $fieldName имя поля или "таб/имя поля"
* @param string|false|null $lang если указанно вернуть для языка (false - для всех языков)
* @param bool $default значение по умолчанию
* @return mixed
*/
public function value($fieldName = '', $lang = null, $default = false)
{
$result = $default;
if (is_null($lang)) {
$lang = $this->locale->current();
}
$fieldActive = $this->fieldActive;
if (! empty($fieldName)) {
$fieldActive = $this->_fieldSelector($fieldName);
}
do {
if (! isset($this->fields[$fieldActive])) { # нет активного поля
break;
}
/** @var Input $field */
$field = $this->fields[$fieldActive]['field'];
# сохраненные значения
$result = $this->_fieldData($fieldActive, $field->getDefault($default));
$field->setValue($result);
$result = $field->value($lang);
} while (false);
return $result;
}
/**
* Формирование значения атрибута name для тегов
* @param int $fieldID ID поля
* @param string $prefix префикс
* @return string
*/
public function _inputNameGenerator($fieldID, $prefix = '')
{
if (isset($this->fields[ $fieldID ])) {
/** @var Input $field */
$field = $this->fields[ $fieldID ]['field'];
if (empty($prefix)) {
return $field->name();
} else {
return $prefix . '[' . $field->name() . ']';
}
}
return '';
}
/**
* Получение значения поля из данных
* @param int $fieldID ID поля
* @param mixed $default значение по умолчанию
* @return mixed
*/
public function _fieldData($fieldID, $default = '')
{
do {
if (! isset($this->fields[ $fieldID ])) {
break;
}
/** @var Input $field */
$field = $this->fields[ $fieldID ]['field'];
$name = $field->name();
if (! isset($this->data[ $name ])) {
break;
}
return $this->data[ $name ];
} while (false);
return $default;
}
/**
* Получение данных о записи
* @return array
*/
public function _data()
{
return $this->data;
}
/**
* Получение данных о записи
* @param string|null $key
* @param mixed $default
* @return mixed
*/
public function getData(?string $key, $default = null)
{
if (! is_null($key)) {
return $this->data[$key] ?? $default;
}
return $this->data;
}
/**
* Установка данных о записи
* @param string $name ключ поля
* @param mixed $value значение
* @return static
*/
public function setData($name, $value)
{
$this->data[$name] = $value;
return $this;
}
/**
* Получение данных в ajax запросах
* @param int $fieldID ID поля
* @param mixed $default значение по умолчанию
* @param bool $reset сбросить кеш
* @return mixed
*/
public function _ajaxData($fieldID, $default = '', $reset = false)
{
do {
if (! isset($this->fields[ $fieldID ])) {
break;
}
/** @var Input $field */
$field = $this->fields[ $fieldID ]['field'];
$name = $field->name();
if (! $this->recordID) {
break;
}
if (array_key_exists($name, $this->data) && ! $reset) {
return $this->data[ $name ];
}
$this->load([ $name ]);
if (array_key_exists($name, $this->data)) {
return $this->data[ $name ];
}
} while (false);
return $default;
}
/**
* Сохранение данных в ajax запросах
* @param int $fieldID ID поля
* @param mixed $value заначение
* @throws AdminFormsException
*/
public function _ajaxSave($fieldID, $value)
{
do {
if (! isset($this->fields[ $fieldID ])) {
break;
}
/** @var Input $field */
$field = $this->fields[ $fieldID ]['field'];
$name = $field->name();
if (! $this->recordID) {
break;
}
$this->save([$name => $value]);
unset($this->data[$name]);
} while (false);
}
/**
* Сохранение данных формы
* @param array $data данные для сохранения
* @return bool
* @throws AdminFormsException
*/
public function save($data)
{
if (empty($data)) {
return false;
}
if (! $this->issetSave()) {
throw new AdminFormsException('Error initialization '.static::class.'. Require to initialise method onSave or useModel');
}
foreach ($data as $k => $v) {
if (! isset($this->data[$k])) {
continue;
}
$this->data[$k] = $v;
}
if ($this->toJSON) {
foreach ($this->fields as $v) {
/** @var Input $field */
$field = $v['field'];
$name = $field->name();
if (! isset($data[$name])) {
continue;
}
if (! is_array($data[$name])) {
continue;
}
$lang = $field->lang();
if ($lang === static::LANG_ARRAY) {
continue;
}
$data[$name] = json_encode($data[$name], JSON_UNESCAPED_UNICODE);
}
}
$inserted = empty($this->recordID);
$this->trigger('saving', ['data' => &$data, 'recordID' => $this->recordID]);
$response = call_user_func($this->_onSave, $this->recordID, $data);
if (! $this->recordID && $response) {
$this->recordID = $response;
}
$this->trigger('saved', ['data' => &$data, 'response' => &$response, 'recordID' => $this->recordID, 'inserted' => $inserted]);
return $this->response($response);
}
/**
* Загрузка данных для формы
* @param array $fields массив требуемых полей
* @throws AdminFormsException
* @return void
*/
public function load(array $fields = [])
{
if (! $this->issetLoad()) {
throw new AdminFormsException('Error initialization '.static::class.'. Require to initialise method onLoad or useModel');
}
if (! $this->recordID) {
$this->data = [];
return;
}
if (empty($fields)) {
foreach ($this->fields as $v) {
/** @var Input $field */
$field = $v['field'];
$fields = array_merge($fields, $field->fieldsList());
}
$fields = array_unique($fields);
}
$this->trigger('loading', ['fields' => &$fields]);
$data = $this->response(
call_user_func($this->_onLoad, $this->recordID, $fields)
);
if (! is_array($data)) {
$data = [];
}
$this->trigger('loaded', ['data' => &$data]);
foreach ($this->fields as $v) {
/** @var Input $field */
$field = $v['field'];
$name = $field->name();
if (! isset($data[$name])) {
continue;
}
$clean = $field->clean();
if (($clean == TYPE_ARRAY || $clean >= TYPE_ARRAY_BOOL) && is_string($data[$name])) {
$data[$name] = json_decode($data[$name], true);
if (! is_array($data[$name])) {
$data[$name] = [];
}
}
}
$this->data = array_merge($this->data, $data);
}
/**
* Получение URL для обработки ajax запросов
* @param string $action название события
* @return string
*/
public function ajaxUrl($action = '')
{
return Url::admin($this->getControllerName() . '/' . $this->getControllerAction()) .
$this->_ajaxParams . '&form_id=' . $this->id . '&record_id=' . HTML::escape($this->recordID()) .
'&' . $this->ajaxAction . '=' . $action;
}
/**
* Добавление параметров к ajax запросам
* @param string|array $data
* @return static
*/
public function ajaxParams($data)
{
if (is_array($data)) {
$data = Url::query($data, [], '&');
}
if (! empty($data) && mb_strpos($data, '&') === false) {
$data = '&' . $data;
}
$this->_ajaxParams = $data;
return $this;
}
/**
* Название GET переменной - параметра содержащей ajax действие
* @return string
*/
public function ajaxAction()
{
return $this->ajaxAction;
}
/**
* Обработчик ajax запросов
* @return \bff\http\Response|void
*/
public function ajax()
{
if ($this->input->getpost('form_id', TYPE_STR) != $this->id) {
return;
}
if ($this->ajaxProcessed) {
return;
}
$this->ajaxProcessed = true;
$recordID = $this->input->getpost('record_id', TYPE_STR);
if ($recordID) {
$this->setRecordID($recordID);
}
$act = $this->input->getpost($this->ajaxAction, TYPE_STR);
$response = [];
switch ($act) {
case 'save': # сабмит формы
$id = $this->input->postget('id', TYPE_STR);
$this->recordSubmit($id, [], $response);
if (! isset($response['reload']) && $this->reload) {
$response['reload'] = $this->reload;
}
break;
case 'tab-init': # инициализация формы в табе
$this->applyWhen();
$id = $this->input->postget('recordID', TYPE_UINT);
$this->wrapper()->hide();
$recordData = ['recordID' => $id];
$response['form'] = $this->view($recordData);
break;
case 'field_action':
$this->applyWhen();
$unionID = $this->input->getpost('union_id', TYPE_UINT);
if ($unionID && isset($this->fields[$unionID])) {
/** @var Fields\Union */
$union = $this->fields[$unionID]['field'];
if ($union instanceof Fields\Union) {
$response = $union->ajax();
break;
}
}
$fieldID = $this->input->getpost('field_id', TYPE_UINT);
if ($fieldID && isset($this->fields[$fieldID])) {
/** @var Input $field */
$field = $this->fields[$fieldID]['field'];
$response = $field->ajax();
break;
}
break;
case 'reloadif':
$response = $this->ajaxHandlerReloadIf();
break;
default:
$this->applyWhen();
$response = false;
if (isset($this->ajaxHandlers[$act])) {
$response = call_user_func($this->ajaxHandlers[$act]);
}
}
if ($response === false) {
return;
}
$this->ajaxResponseForm($response);
}
/**
* Установка обработчика ajax запроса для формы поля
* @param string $action
* @param callable|null $callable
* @return static|callable|null
*/
public function formAjaxHandler($action, $callable)
{
if (is_null($callable)) {
return $this->ajaxHandlers[$action] ?? null;
}
$this->ajaxHandlers[$action] = $callable;
return $this;
}
/**
* Обработчик ajax запроса ReloadIf
* @return array
*/
protected function ajaxHandlerReloadIf()
{
$response = [];
do {
$this->applyWhen();
$masterID = $this->input->post('master', TYPE_UINT);
$value = $this->input->post('value', TYPE_NOCLEAN);
$formData = $this->input->post('formData', TYPE_STR);
if (! $masterID || ! isset($this->fields[$masterID])) {
break;
}
/** @var Input $master */
$master = $this->fields[$masterID]['field'];
if (empty($master->reloadIF['master'])) {
break;
}
# Загрузим данные в форму
parse_str($formData, $formData);
$formData = $this->_validate($formData);
$this->errors->clear();
# Вызовем все функции 'condition' привязанне к полю 'master', для возможности изменения $formData
foreach ($master->reloadIF['master'] as $v) {
if (empty($v['condition'])) {
continue;
}
call_user_func($v['condition'], $value, $master, ['data' => & $formData, 'response' => & $response]);
}
$this->data = array_merge($this->data, $formData);
# Рендерим контент формы
$this->reloadIfContent = [];
$this->content();
$reload = [];
$allowOther = function ($slave) use ($master) {
/** @var Input $slave */
if (count($slave->reloadIF['slave']) == 1) {
return true;
}
foreach ($slave->reloadIF['slave'] as $v) {
if (! in_array($v['mode'], ['present', 'absent'])) {
continue;
}
/** @var Input $field */
$field = $v['master'];
if ($field->id() == $master->id()) {
continue;
}
$value = $field->value();
switch ($v['mode']) {
case 'present':
if (! call_user_func($v['condition'], $value, $field)) {
return false;
}
break;
case 'absent':
if (call_user_func($v['condition'], $value, $field)) {
return false;
}
break;
}
}
return true;
};
foreach ($master->reloadIF['master'] as $v) {
if (empty($v['condition'])) {
continue;
}
/** @var Input $slave */
$slave = $v['slave'];
$id = $slave->id();
if (! array_key_exists($id, $this->reloadIfContent)) {
continue;
}
switch ($v['mode']) {
case 'reload':
if (! call_user_func($v['condition'], $value, $slave)) {
break;
}
$reload[$id] = $this->reloadIfContent[$id];
break;
case 'present':
$reload[$id] = '';
if (call_user_func($v['condition'], $value, $slave)) {
if ($allowOther($slave)) {
$reload[$id] = $this->reloadIfContent[$id];
}
}
break;
case 'absent':
$reload[$id] = '';
if (! call_user_func($v['condition'], $value, $slave)) {
if ($allowOther($slave)) {
$reload[$id] = $this->reloadIfContent[$id];
}
}
break;
}
}
$response['reload'] = $reload;
$response['master'] = [
'id' => $masterID,
'name' => $master->name(),
'value' => $value,
];
$this->appendIncludedStatic($response);
} while(false);
return $response;
}
/**
* @param Input $field
* @param string $content
* @return string
*/
public function _reloadIfSlaveContent($field, $content)
{
if (is_array($this->reloadIfContent)) {
$this->reloadIfContent[$field->id()] = $content;
} else if (is_null($this->reloadIfContent)) {
if (! empty($field->reloadIF['slave'])) {
foreach ($field->reloadIF['slave'] as $v) {
if (! in_array($v['mode'], ['present', 'absent'])) {
continue;
}
/** @var Input $master */
$master = $v['master'];
$value = $master->value();
switch ($v['mode']) {
case 'present':
if (! call_user_func($v['condition'], $value, $field)) {
$content = '';
break 2;
}
break;
case 'absent':
if (call_user_func($v['condition'], $value, $field)) {
$content = '';
break 2;
}
break;
}
}
}
}
return $content;
}
/**
* Управление флагом обработки ajax запросов
* @param bool|null $processed
* @return static|bool
*/
public function ajaxProcessed($processed = null)
{
if (is_null($processed)) {
return $this->ajaxProcessed;
}
$this->ajaxProcessed = $processed;
return $this;
}
/**
* Ключ текущего вложенного действия или false
* @return string|bool
*/
public function subAction()
{
$subAction = $this->input->postget($this->ajaxAction, TYPE_NOTAGS);
if (empty($subAction)) {
return false;
}
return $subAction;
}
/**
* Установка обработчика события проверки целосности данных по событию сабмита формы
* @param callable $callback функция обработчика, аргументы ($recordID, array('data' => & $data) $recordID - ID записи, $data - ссылка массив с данными для сохранения в формате [имяполя => значение]
* обработчик должен быть установлен перед обработчиком события сохранения данных onSave
* возвращаемые значения:
* false - не корректные данные сохранение не происходит, если не срабатывает проверка $this->errors->no() сохранение не происходит
* array - массив с данными для ajax ответа
* @param array $opts доп. параметры
* @return static
*/
public function onSubmit(callable $callback, array $opts = [])
{
$opts['callable'] = $callback;
$this->_onSubmit = $opts;
return $this;
}
/**
* Установка обработчика события проверки целосности данных по событию сабмита формы после сохранения
* @param callable $callback функция обработчика, аргументы
* ($recordID, - ID записи
* array(
* 'data' => & @ref, - ссылка на массив с данными для сохранения, в формате [имяполя => значение]
* 'saved' => & @ref, - ссылка на массив сохраненных данных, в формате [имяполя => значение]
* 'response' => & @ref, - ссылка на массив с данными для ajax ответа форме
* 'inserted' => bool - флаг добавления новой записи
* )
* )
* обработчик должен быть установлен перед обработчиком события сохранения данных onSave
* @param array $opts доп. параметры
* @return static
*/
public function onSubmitAfter(callable $callback, array $opts = [])
{
$opts['callable'] = $callback;
$this->_onSubmitAfter = $opts;
return $this;
}
/** Событие сабмита формы
* @param int $recordID
* @param array $post данные для для сабмита, [] - взять из $_POST
* @param array $response @ref массив для аякс ответа
* @throws
* @return void
*/
public function recordSubmit($recordID, array $post = [], array & $response = [])
{
$this->setRecordID($recordID);
$this->applyWhen();
# валидируем данные
$data = $this->_validate($post);
# событие onSubmit
if (isset($this->_onSubmit['callable']) && is_callable($this->_onSubmit['callable'])) {
$result = $this->response(
call_user_func($this->_onSubmit['callable'], $recordID, ['data' => & $data, 'response' => & $response])
);
if ($result === false) {
return;
}
if (is_array($result)) {
$response = $result;
}
}
if (! $this->errors->no()) {
return;
}
# сохраняем данные
$res = $this->save($data);
$inserted = empty($recordID) && is_int($res) && $res > 0;
if (empty($this->recordID)) {
$this->setRecordID($res);
}
# действия после сохранения (перенос картинок и файлов)
$after = [];
if ($this->recordID) {
$after = $this->_validateAfter($inserted, $data);
}
# событие onSubmitAfter
if (isset($this->_onSubmitAfter['callable']) && is_callable($this->_onSubmitAfter['callable'])) {
$this->response(call_user_func($this->_onSubmitAfter['callable'], $this->recordID, [
'data' => & $after,
'saved' => & $data,
'response' => & $response,
'inserted' => $inserted,
]));
}
}
/**
* Событие перед удалением записи, если есть ошибка метод recordDelete не вызывается
* @param int $recordID
* @return void
*/
public function recordBeforeDelete($recordID)
{
if (empty($recordID)) {
return;
}
$this->setRecordID($recordID);
$this->applyWhen();
foreach ($this->fields as $v) {
/** @var Input $field */
$field = $v['field'];
if (method_exists($field, 'recordBeforeDelete')) {
$field->recordBeforeDelete($recordID);
}
}
}
/**
* Событие удаления записи
* @param int $recordID
* @return void
*/
public function recordDelete($recordID)
{
if (empty($recordID)) {
return;
}
$this->setRecordID($recordID);
$this->applyWhen();
foreach ($this->fields as $v) {
/** @var Input $field */
$field = $v['field'];
if (method_exists($field, 'recordDelete')) {
$field->recordDelete($recordID);
}
}
}
/**
* Установка названия таба формы, если в списке используется несколько форм
* @param string|callable $title
* @return static
*/
public function setTitle($title)
{
$this->title = $title;
$this->wrapper()->title($title);
return $this;
}
/**
* Установка названия JS объекта списка, и урл для открытия формы для возможности управления историей ($_GET['gtab'])
* @param string $jsObject имя JS объекта (false - отключить)
* @param string $url
* @return static
*/
public function jsHistory($jsObject, $url = '')
{
if ($this->_history !== false) {
if ($jsObject == false) {
$this->_history = false;
} else {
$this->_history = ['object' => $jsObject, 'url' => $url];
}
}
return $this;
}
/**
* Выбор поля по имени или "таб/имя поля"
* @param string|int $fieldName имя поля или fieldID
* @return int ID поля
*/
public function _fieldSelector($fieldName)
{
$fieldID = false;
do {
if (empty($fieldName)) {
break;
}
if (is_int($fieldName) && isset($this->fields[$fieldName])) {
$fieldID = $fieldName;
break;
}
$tabActive = $this->tabActive;
$fieldName = explode('/', $fieldName);
if (count($fieldName) == 2) { # "таб/имя поля"
foreach ($this->tabs as $k => $v) {
if ($v['name'] == $fieldName[0]) {
$tabActive = $k;
break;
}
}
$name = $fieldName[1];
} else {
$name = $fieldName[0];
}
if (empty($name)) {
break;
}
# название поля в активном табе
foreach ($this->fields as $k => $v) {
if ($v['tab'] != $tabActive) {
continue;
}
/** @var Input $field */
$field = $v['field'];
if ($field->name() == $name) {
$fieldID = $k;
break 2;
}
}
# если не нашли и не указан таб, ищем во всех табах
if (count($fieldName) == 1) {
foreach ($this->fields as $k => $v) {
/** @var Input $field */
$field = $v['field'];
if ($field->name() == $name) {
$fieldID = $k;
break 2;
}
}
}
} while (false);
return $fieldID;
}
/**
* Сделать поле активным, выбор поля по имени или "таб/имя поля"
* @param string|int $fieldName имя поля или fieldID
* @param string|int $fieldInUnion имя поля или fieldID внутри группы если $fieldName - объединение полей
* @return static
*/
public function _fieldActivate($fieldName, $fieldInUnion = 0)
{
$fieldID = $this->_fieldSelector($fieldName);
if (isset($this->fields[$fieldID])) {
# сделаем поле активным
$field = $this->fields[$fieldID]['field'];
$this->fieldActive = $fieldID;
$this->tabActive = $this->fields[$fieldID]['tab'];
$this->unionActive = false;
if ($field instanceof Fields\Union) {
$this->unionActive = $fieldID;
if ($fieldInUnion) {
$field->field($fieldInUnion);
}
}
}
return $this;
}
/**
* Получения объекта поля по имени или "таб/имя поля"
* @param string|int $fieldName имя поля или fieldID
* @return Input|bool
*/
public function field($fieldName)
{
$fieldID = $this->_fieldSelector($fieldName);
if (isset($this->fields[$fieldID])) {
return $this->fields[$fieldID]['field'];
}
return false;
}
/**
* Получения объекта активного поля
* @param bool $inUnion true для объединения полей, возвращаем активное поле внутри объединения
* @return Input|bool
*/
public function _activeField($inUnion = true)
{
do {
if ($inUnion && $this->unionActive && isset($this->fields[$this->unionActive])) {
/** @var Fields\Union */
$union = $this->fields[$this->unionActive]['field'];
if ($union instanceof Fields\Union) {
$result = $union->_activeField();
if ($result !== false) {
return $result;
}
}
}
if (! isset($this->fields[$this->fieldActive])) {
break;
}
/** @var Input $field */
return $this->fields[$this->fieldActive]['field'];
} while (false);
return false;
}
/**
* Получение активного поля
* @param bool $name
* @return mixed
*/
public function _getField($name = false)
{
if ($name) {
return $this->field($name);
}
return $this->_activeField();
}
/**
* Выполнить callback, если условие выполняется
* @param bool|callable $condition условие или функция ($form) возвращающая bool
* @param callable $action действие ($form), выполняется если $condition === true
* @param array $opts параметры
* @return static
*/
public function when($condition, callable $action, array $opts = [])
{
$opts['condition'] = $condition;
$opts['action'] = $action;
$this->when[] = $opts;
return $this;
}
/**
* Выполнить callback, если добавляется новая запись
* @param callable $action действие ($form)
* @param array $opts параметры
* @return static
*/
public function whenNew(callable $action, array $opts = [])
{
$this->when(function () {
$id = $this->recordID();
return empty($id);
}, $action, $opts);
return $this;
}
/**
* Выполнить callback, если запись редактируется
* @param callable $action действие ($form)
* @param array $opts параметры
* @return static
*/
public function whenEdit(callable $action, array $opts = [])
{
$this->when(function () {
$id = $this->recordID();
return ! empty($id);
}, $action, $opts);
return $this;
}
/**
* Проверить when-условия и выполнить callback
*/
protected function applyWhen()
{
if ($this->whenApplied) {
return;
}
$this->whenApplied = true;
foreach ($this->when as $v) {
if (! isset($v['condition'])) {
continue;
}
if (! is_callable($v['action'])) {
continue;
}
if (is_callable($v['condition'])) {
$condition = call_user_func($v['condition'], $this);
} else {
$condition = $v['condition'];
}
if ($condition !== true) {
continue;
}
$this->response(call_user_func($v['action'], $this));
}
}
/**
* Определить количество элементов формы
* @param string $name
* @return array|int
*/
public function count($name = '')
{
$result = [
'fields' => count($this->fields),
'tabs' => count($this->tabs),
'buttons' => count($this->buttons),
];
if (! empty($name)) {
return $result[$name] ?? 0;
}
return $result;
}
# === добавление табов и полей ===
/**
* Добавление или выбор таба
* @param string $name имя таба
* @param string $title заголовок
* @param array $opts доп.параметры
* @return static
*/
public function tab($name, $title = '', array $opts = [])
{
do {
foreach ($this->tabs as $k => $v) {
if ($v['name'] == $name) {
$this->tabActive = $k;
$this->unionActive = false;
break 2;
}
}
$id = sizeof($this->tabs) + 1;
$opts = $this->defaults($opts, [
'id' => $id,
'name' => $name,
'title' => $title,
'hidden' => false,
'table' => true,
'stretch' => false,
'url' => false,
'buttons' => true,
'ajax' => false,
'counter' => null,
'style' => 'default',
'icon' => '',
'collapsed' => false,
'default' => false,
'priority' => false,
'wrapperA' => null, # static function($html, $o) { $o['attr']['foo'] = 'bar'; return 'foo'.$html;}
'wrapperLI' => null, # static function($html, $o) { $o['attr']['foo'] = 'bar'; return 'foo'.$html;}
]);
$this->tabs[$id] = $opts;
$this->unionActive = false;
$this->tabActive = $id;
} while (false);
return $this;
}
/**
* Проверка существования активного таба и возможности добавления в него не специальных полей
* @return void
*/
protected function checkActiveTab()
{
if (! isset($this->tabs[ $this->tabActive ])) { # поля могут добавлятся только внутри таба
if (empty($this->tabs)) {
# если нет табов создадим один скрытый таб
$this->tab('h', 'noname', ['hidden' => true]);
} else {
foreach ($this->tabs as $k => $v) {
$this->tabActive = $k;
break;
}
}
}
# проверим возможность добавления полей в таб
if (! $this->tabs[ $this->tabActive ]['table']) {
foreach ($this->tabs as $k => $v) {
if ($v['table']) {
$this->tabActive = $k;
break;
}
}
}
if (! $this->tabs[ $this->tabActive ]['table']) {
$this->tab('h', ' ');
}
}
/**
* Добавление SEO таба
* @param string $group группа страницы
* @param string $key имя страницы
* @param string $title заголовок таба
* @param array $opts доп. параметры
* string 'prefix' - префикс SEO таба, если используется более одного SEO таба в форме
* @return static
*/
public function tabSEO(string $group, string $key, $title = 'SEO', array $opts = [])
{
$opts = $this->defaults($opts, [
'prefix' => '',
]);
do {
$tabActive = $this->tabActive;
$this->tab('seo_' . $opts['prefix'] . $group . $key, $title);
SEO::templateForm($group, $key, $this, $opts);
$this->tabActive = $tabActive;
} while (false);
return $this;
}
/**
* Добавление таба-ссылки на страницу
* @param string $name имя таба
* @param string $title заголовок
* @param string $url ссылка на страницу
* @param array $opts доп.параметры
* @return static
*/
public function tabURL($name, $title, $url, array $opts = [])
{
$tabActive = $this->tabActive;
$opts['table'] = false;
$opts['url'] = $url;
$this->tab($name, $title, $opts);
$this->tabActive = $tabActive;
return $this;
}
/**
* Добавление таба с ajax загрузкой контента по Url
* @param string $name имя таба
* @param string $title заголовок
* @param string $url ссылка для получения url, в ответе должен быть ключ html
* @param array $opts доп.параметры
* @return static
*/
public function tabAjax($name, $title, $url, array $opts = [])
{
$tabActive = $this->tabActive;
$opts['table'] = false;
$opts['ajax'] = $url;
$opts['buttons'] = false;
$this->tab($name, $title, $opts);
$this->tabActive = $tabActive;
return $this;
}
/**
* Установка обработчика события добавления поля
* @param callable $callback функция обработчика, аргументы (Fields\Input $field)
* @return int index добавленного обработчика (для возможности удаления)
*/
public function onFieldAdd(callable $callback)
{
$this->_onFieldAdd[] = $callback;
return array_key_last($this->_onFieldAdd);
}
/**
* Удаление обработчика события добавления поля
* @param int $index index добавленного ранее обработчика
* @return static
*/
public function removeOnFieldAdd(int $index)
{
unset($this->_onFieldAdd[$index]);
return $this;
}
/**
* Добавление поля
* @param Fields\Input $field
* @param bool $checkTab выполнить проверку на таб
* @return void
*/
protected function fieldAdd($field, $checkTab = true)
{
$field->setProperties(['form' => $this]);
$field->init();
# add field handlers: before
foreach($this->_onFieldAdd as $callback) {
$callback($field, false/* before */);
}
if ($this->unionActive && isset($this->fields[$this->unionActive]) && $field->inUnionAllowed()) {
/** @var Fields\Union $union */
$union = $this->fields[$this->unionActive]['field'];
$union->fieldAdd($field);
# add field handlers: after
foreach($this->_onFieldAdd as $callback) {
$callback($field, true/* after */);
}
return;
}
$checkTab && $this->checkActiveTab();
$id = sizeof($this->fields) + 1;
$field->setID($id);
$max = 0;
foreach ($this->fields as $v) {
if ($max < $v['priority']) {
$max = $v['priority'];
}
}
$this->fields[$id] = [
'id' => $id,
'field' => $field,
'priority' => $max + 10,
'tab' => $this->tabActive,
];
$this->fieldActive = $id;
# add field handlers: after
foreach($this->_onFieldAdd as $callback) {
$callback($field, true/* after */);
}
}
/**
* Дополнение дефолтного значения $default для lang инпутов text, textarea и т.д.
* @param string|array $value значение
* @param bool $lang использовать мультиязычность
* true - использовать - вернет массив со всеми языками
* false - не использовать - вернет строку на default языке или для первого языка
* @return array|string
*/
public function defaultLang($value = '', $lang = false)
{
if ($lang) {
return Lang::fill([], $value);
} else {
if (is_array($value)) {
return Lang::value($value);
}
return $value;
}
}
/**
* Сделать поле активным, выбор поля по имени или "таб/имя поля" alias для функции _fieldActivate
* @param string|int $fieldName имя поля или fieldID
* @return static
*/
public function to($fieldName)
{
return $this->_fieldActivate($fieldName);
}
/**
* Добавление поля input text
* @param string $name имя
* @param string $title заголовок
* @param array|string $default значение по умолчанию
* @param bool $lang использовать мультиязычность
* @param array $opts доп.параметры
* @return static
*/
public function text($name, $title = '', $default = [], $lang = true, array $opts = [])
{
if (empty($default) && $lang === static::LANG_NONE) {
$default = '';
}
$opts['title'] = $title;
$opts['default'] = $this->defaultLang($default, $lang);
$opts['lang'] = $lang;
if (! isset($opts['stretch'])) {
$opts['stretch'] = true;
}
$opts['clean'] = ($lang === static::LANG_NONE ? TYPE_STR : TYPE_ARRAY_STR);
$f = new Fields\Text($name, $opts);
$this->fieldAdd($f);
return $this;
}
/**
* Получение активного поля типа Text
* @param string|bool $name имя поля false - активное поле
* @return Fields\Text
*/
public function getText($name = false)
{
return $this->_getField($name);
}
/**
* Добавление поля ввода многострочного текста