config = $this->makeConfig($controller->formConfig ?: $this->formConfig, $this->requiredConfig); $this->config->modelClass = Str::normalizeClassName($this->config->modelClass); } /** * Initialize the form configuration against a model and context value. * This will process the configuration found in the `$formConfig` property * and prepare the Form widget, which is the underlying tool used for * actually rendering the form. The model used by this form is passed * to this behavior via this method as the first argument. * * @see \Backend\Widgets\Form * @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model $model * @param string $context Form context * @return void */ public function initForm($model, $context = null) { $context = $this->context = $context ?? $this->formGetContext(); /* * Each page can supply a unique form definition, if desired */ $formFields = $this->getConfig("{$context}[form]", $this->config->form); $config = $this->makeConfig($formFields); $config->model = $model; $config->arrayName = class_basename($model); $config->context = $context; /* * Form Widget with extensibility */ $this->formWidget = $this->makeWidget('Backend\Widgets\Form', $config); // Setup the default preview mode on form initialization if the context is preview if ($config->context === 'preview') { $this->formWidget->previewMode = true; } $this->formWidget->bindEvent('form.extendFieldsBefore', function () { $this->controller->formExtendFieldsBefore($this->formWidget); }); $this->formWidget->bindEvent('form.extendFields', function ($fields) { $this->controller->formExtendFields($this->formWidget, $fields); }); $this->formWidget->bindEvent('form.beforeRefresh', function ($holder) { $result = $this->controller->formExtendRefreshData($this->formWidget, $holder->data); if (is_array($result)) { $holder->data = $result; } }); $this->formWidget->bindEvent('form.refreshFields', function ($fields) { return $this->controller->formExtendRefreshFields($this->formWidget, $fields); }); $this->formWidget->bindEvent('form.refresh', function ($result) { return $this->controller->formExtendRefreshResults($this->formWidget, $result); }); $this->formWidget->bindToController(); /* * Detected Relation controller behavior */ if ($this->controller->isClassExtendedWith(\Backend\Behaviors\RelationController::class)) { $this->controller->initRelation(clone $model); } $this->prepareVars($model); $this->model = $model; } /** * Prepares commonly used view data. * @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model $model */ protected function prepareVars($model) { $this->controller->vars['formModel'] = $model; $this->controller->vars['formConfig'] = $this->getConfig(); $this->controller->vars['formContext'] = $this->formGetContext(); $this->controller->vars['formController'] = $this; $this->controller->vars['formRecordName'] = Lang::get($this->getConfig('name', 'backend::lang.model.name')); } // // Create // /** * Controller "create" action used for creating new model records. * * @param string $context Form context * @return void */ public function create($context = null) { try { $this->context = strlen($context) ? $context : $this->getConfig('create[context]', self::CONTEXT_CREATE); $this->controller->pageTitle = $this->controller->pageTitle ?: $this->getLang( "{$this->context}[title]", 'backend::lang.form.create_title' ); $model = $this->controller->formCreateModelObject(); $model = $this->controller->formExtendModel($model) ?: $model; $this->initForm($model); } catch (Exception $ex) { $this->controller->handleError($ex); } } /** * AJAX handler "onSave" called from the create action and * primarily used for creating new records. * * This handler will invoke the unique controller overrides * `formBeforeCreate` and `formAfterCreate`. * * @param string $context Form context * @return \Illuminate\Http\RedirectResponse|void */ public function create_onSave($context = null) { $this->context = strlen($context) ? $context : $this->getConfig('create[context]', self::CONTEXT_CREATE); $model = $this->controller->formCreateModelObject(); $model = $this->controller->formExtendModel($model) ?: $model; $this->initForm($model); $this->controller->formBeforeSave($model); $this->controller->formBeforeCreate($model); $modelsToSave = $this->prepareModelsToSave($model, $this->formWidget->getSaveData()); Db::transaction(function () use ($modelsToSave) { foreach ($modelsToSave as $modelToSave) { $modelToSave->save(null, $this->formWidget->getSessionKey()); } }); $this->controller->formAfterSave($model); $this->controller->formAfterCreate($model); Flash::success($this->getLang("{$this->context}[flashSave]", 'backend::lang.form.create_success')); if ($redirect = $this->makeRedirect($this->context, $model)) { return $redirect; } } // // Update // /** * Controller "update" action used for updating existing model records. * This action takes a record identifier (primary key of the model) * to locate the record used for sourcing the existing form values. * * @param int $recordId Record identifier * @param string $context Form context * @return void */ public function update($recordId = null, $context = null) { try { $this->context = strlen($context) ? $context : $this->getConfig('update[context]', self::CONTEXT_UPDATE); $this->controller->pageTitle = $this->controller->pageTitle ?: $this->getLang( "{$this->context}[title]", 'backend::lang.form.update_title' ); $model = $this->controller->formFindModelObject($recordId); $this->initForm($model); } catch (Exception $ex) { $this->controller->handleError($ex); } } /** * AJAX handler "onSave" called from the update action and * primarily used for updating existing records. * * This handler will invoke the unique controller overrides * `formBeforeUpdate` and `formAfterUpdate`. * * @param int $recordId Record identifier * @param string $context Form context * @return \Illuminate\Http\RedirectResponse|void * @throws \Winter\Storm\Exception\ApplicationException if the provided recordId is not found */ public function update_onSave($recordId = null, $context = null) { $this->context = strlen($context) ? $context : $this->getConfig('update[context]', self::CONTEXT_UPDATE); $model = $this->controller->formFindModelObject($recordId); $this->initForm($model); $this->controller->formBeforeSave($model); $this->controller->formBeforeUpdate($model); $modelsToSave = $this->prepareModelsToSave($model, $this->formWidget->getSaveData()); Db::transaction(function () use ($modelsToSave) { foreach ($modelsToSave as $modelToSave) { $modelToSave->save(null, $this->formWidget->getSessionKey()); } }); $this->controller->formAfterSave($model); $this->controller->formAfterUpdate($model); Flash::success($this->getLang("{$this->context}[flashSave]", 'backend::lang.form.update_success')); if ($redirect = $this->makeRedirect($this->context, $model)) { return $redirect; } } /** * AJAX handler "onDelete" called from the update action and * used for deleting existing records. * * This handler will invoke the unique controller override * `formAfterDelete`. * * @param int $recordId Record identifier * @return \Illuminate\Http\RedirectResponse|void * @throws \Winter\Storm\Exception\ApplicationException if the provided recordId is not found * @throws Exception if there is no primary key on the model */ public function update_onDelete($recordId = null) { $this->context = $this->getConfig('update[context]', self::CONTEXT_UPDATE); $model = $this->controller->formFindModelObject($recordId); $this->initForm($model); $model->delete(); $this->controller->formAfterDelete($model); Flash::success($this->getLang("{$this->context}[flashDelete]", 'backend::lang.form.delete_success')); if ($redirect = $this->makeRedirect('delete', $model)) { return $redirect; } } // // Preview // /** * Controller "preview" action used for viewing existing model records. * This action takes a record identifier (primary key of the model) * to locate the record used for sourcing the existing preview data. * * @param int $recordId Record identifier * @param string $context Form context * @return void */ public function preview($recordId = null, $context = null) { try { $this->context = strlen($context) ? $context : $this->getConfig('preview[context]', self::CONTEXT_PREVIEW); $this->controller->pageTitle = $this->controller->pageTitle ?: $this->getLang( "{$this->context}[title]", 'backend::lang.form.preview_title' ); $model = $this->controller->formFindModelObject($recordId); $this->initForm($model); } catch (Exception $ex) { $this->controller->handleError($ex); } } // // Utils // /** * Method to render the prepared form markup. This method is usually * called from a view file. * * formRender() ?> * * The first argument supports an array of render options. The supported * options can be found via the `render` method of the Form widget class. * * formRender(['preview' => true, section' => 'primary']) ?> * * @see \Backend\Widgets\Form * @param array $options Render options * @return string Rendered HTML for the form. * @throws \Winter\Storm\Exception\ApplicationException if the Form Widget isn't set */ public function formRender($options = []) { if (!$this->formWidget) { throw new ApplicationException(Lang::get('backend::lang.form.behavior_not_ready')); } return $this->formWidget->render($options); } /** * Returns the model initialized by this form behavior. * The model will be provided by one of the page actions or AJAX * handlers via the `initForm` method. * * @return \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model */ public function formGetModel() { return $this->model; } /** * Returns the active form context, either obtained from the postback * variable called `form_context` or detected from the configuration, * or routing parameters. * * @return string */ public function formGetContext() { return $this->context; } /** * Internal method used to prepare the form model object. * * @return \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model */ protected function createModel() { $class = $this->config->modelClass; return new $class; } /** * Returns a Redirect object based on supplied context and parses * the model primary key. * * @param string $context Redirect context, eg: create, update, delete * @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model $model The active model to parse in it's ID and attributes. * @return \Illuminate\Http\RedirectResponse */ public function makeRedirect($context = null, $model = null) { $redirectUrl = null; if (post('close') && !ends_with($context, '-close')) { $context .= '-close'; } if (post('refresh', false)) { return Redirect::refresh(); } if (post('new', false)) { return Redirect::to($this->controller->actionUrl('create')); } if (post('redirect', true)) { $redirectUrl = $this->controller->formGetRedirectUrl($context, $model); } if ($model && $redirectUrl) { $redirectUrl = RouterHelper::replaceParameters($model, $redirectUrl); } if (starts_with($redirectUrl, 'http://') || starts_with($redirectUrl, 'https://')) { // Process absolute redirects $redirect = Redirect::to($redirectUrl); } else { // Process relative redirects $redirect = $redirectUrl ? Backend::redirect($redirectUrl) : null; } return $redirect; } /** * Returns a redirect URL from the config based on supplied context. * Otherwise the default redirect is used. Relative URLs are treated as * backend URLs. * * @param string $context Redirect context, eg: create, update, delete. * @param Model $model The active model. * @return string */ public function formGetRedirectUrl($context = null, $model = null) { $redirectContext = explode('-', $context, 2)[0]; $redirectSource = ends_with($context, '-close') ? 'redirectClose' : 'redirect'; // Get the redirect for the provided context $redirects = [$context => $this->getConfig("{$redirectContext}[{$redirectSource}]", '')]; // Assign the default redirect afterwards to prevent the // source for the default redirect being default[redirect] $redirects['default'] = $this->getConfig('defaultRedirect', ''); if (empty($redirects[$context])) { return $redirects['default']; } return $redirects[$context]; } /** * Parses in some default variables to a language string defined in config. * * @param string $name Configuration property containing the language string * @param string $default A default language string to use if the config is not found * @param array $extras Any extra params to include in the language string variables * @return string The translated string. */ protected function getLang($name, $default = null, $extras = []) { $name = $this->getConfig($name, $default); $vars = [ 'name' => Lang::get($this->getConfig('name', 'backend::lang.model.name')) ]; $vars = array_merge($vars, $extras); return Lang::get($name, $vars); } // // Pass-through Helpers // /** * View helper to render a single form field. * * formRenderField('field_name') ?> * * @param string $name Field name * @param array $options (e.g. ['useContainer'=>false]) * @return string HTML markup */ public function formRenderField($name, $options = []) { return $this->formWidget->renderField($name, $options); } /** * View helper to render the form in preview mode. * * formRenderPreview() ?> * * @return string The form HTML markup. * @throws \Winter\Storm\Exception\ApplicationException if the Form Widget isn't set */ public function formRenderPreview() { return $this->formRender(['preview' => true]); } /** * View helper to check if a form tab has fields in the * non-tabbed section (outside fields). * * formHasOutsideFields()): ?> * * * * @return bool */ public function formHasOutsideFields() { return $this->formWidget->getTab('outside')->hasFields(); } /** * View helper to render the form fields belonging to the * non-tabbed section (outside form fields). * * formRenderOutsideFields() ?> * * @return string HTML markup * @throws \Winter\Storm\Exception\ApplicationException if the Form Widget isn't set */ public function formRenderOutsideFields() { return $this->formRender(['section' => 'outside']); } /** * View helper to check if a form tab has fields in the * primary tab section. * * formHasPrimaryTabs()): ?> * * * * @return bool */ public function formHasPrimaryTabs() { return $this->formWidget->getTab('primary')->hasFields(); } /** * View helper to render the form fields belonging to the * primary tabs section. * * formRenderPrimaryTabs() ?> * * @return string HTML markup * @throws \Winter\Storm\Exception\ApplicationException if the Form Widget isn't set */ public function formRenderPrimaryTabs() { return $this->formRender(['section' => 'primary']); } /** * View helper to check if a form tab has fields in the * secondary tab section. * * formHasSecondaryTabs()): ?> * * * * @return bool */ public function formHasSecondaryTabs() { return $this->formWidget->getTab('secondary')->hasFields(); } /** * View helper to render the form fields belonging to the * secondary tabs section. * * formRenderSecondaryTabs() ?> * * @return string HTML markup * @throws \Winter\Storm\Exception\ApplicationException if the Form Widget isn't set */ public function formRenderSecondaryTabs() { return $this->formRender(['section' => 'secondary']); } /** * View helper to render the previous/next record navigation for the record * being edited, relative to its sibling records in the controller's list. * * formRenderRecordNavigation() ?> * * Renders nothing unless the `recordNavigation` config is enabled (the * default), the controller also implements the ListController behavior, and * an existing record is being viewed. * * @return string HTML markup (empty string when navigation is unavailable) */ public function formRenderRecordNavigation(): string { $navigation = $this->formGetRecordNavigation(); if ($navigation === null || $navigation['current'] === null) { return ''; } return $this->formMakePartial('record_navigation', [ 'navigation' => $navigation, 'navigationContext' => $this->context, ]); } /** * Resolves the position of the current record within the controller's list * and the neighboring record keys used for previous/next navigation. * * The sibling set comes from the ListController's prepared query, so it * reflects the active filters, search and sorting exactly as the user left * the list. Ordered keys are read with a single portable `pluck` and the * position is resolved in PHP — no driver-specific SQL — so it behaves * identically across every database Winter supports. * * @param \Winter\Storm\Database\Model|null $model * @return array{previous: mixed, next: mixed, current: int|null, total: int}|null */ public function formGetRecordNavigation($model = null): ?array { if (!$this->getConfig('recordNavigation', true)) { return null; } $model = $model ?: $this->model; if (!$model || !$model->exists) { return null; } if (!$this->controller->isClassExtendedWith(\Backend\Behaviors\ListController::class)) { return null; } $this->controller->makeLists(); $listWidget = $this->controller->listGetWidget(); if (!$listWidget) { return null; } $keys = $listWidget->prepareQuery()->pluck($model->getQualifiedKeyName())->all(); return static::resolveRecordPosition($keys, $model->getKey()); } /** * Pure position math for record navigation: given an ordered list of record * keys and the current key, returns the navigation descriptor. Performs no * database access, so it is trivially unit-testable and identical on every * driver. * * @param array $keys Ordered list of record keys. * @param mixed $currentKey The key of the record being viewed. * @return array{previous: mixed, next: mixed, current: int|null, total: int} */ public static function resolveRecordPosition(array $keys, $currentKey): array { $keys = array_values($keys); $total = count($keys); $position = null; foreach ($keys as $index => $key) { if ((string) $key === (string) $currentKey) { $position = $index; break; } } return [ 'previous' => ($position !== null && $position > 0) ? $keys[$position - 1] : null, 'next' => ($position !== null && $position < $total - 1) ? $keys[$position + 1] : null, 'current' => $position === null ? null : $position + 1, 'total' => $total, ]; } /** * Returns the form widget used by this behavior. * * @return \Backend\Widgets\Form */ public function formGetWidget() { return $this->formWidget; } /** * Returns a unique ID for the form widget used by this behavior. * This is useful for dealing with identifiers in the markup. * *
...
* * A suffix may be used passed as the first argument to reuse * the identifier in other areas. * * * * @param string $suffix * @return string */ public function formGetId($suffix = null) { return $this->formWidget->getId($suffix); } /** * Helper to get the form session key. * * @return string */ public function formGetSessionKey() { return $this->formWidget->getSessionKey(); } // // Overrides // /** * Called before the creation or updating form is saved. * @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model */ public function formBeforeSave($model) { } /** * Called after the creation or updating form is saved. * @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model */ public function formAfterSave($model) { } /** * Called before the creation form is saved. * @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model */ public function formBeforeCreate($model) { } /** * Called after the creation form is saved. * @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model */ public function formAfterCreate($model) { } /** * Called before the updating form is saved. * @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model */ public function formBeforeUpdate($model) { } /** * Called after the updating form is saved. * @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model */ public function formAfterUpdate($model) { } /** * Called after the form model is deleted. * @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model */ public function formAfterDelete($model) { } /** * Finds a Model record by its primary identifier, used by update actions. This logic * can be changed by overriding it in the controller. * @param string $recordId * @return \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model * @throws \Winter\Storm\Exception\ApplicationException if the provided recordId is not found */ public function formFindModelObject($recordId) { if (!strlen($recordId)) { throw new ApplicationException($this->getLang('not-found-message', 'backend::lang.form.missing_id')); } $model = $this->controller->formCreateModelObject(); /* * Prepare query and find model record */ $query = $model->newQuery(); $this->controller->formExtendQuery($query); $result = $query->find($recordId); if (!$result) { throw new ApplicationException($this->getLang('not-found-message', 'backend::lang.form.not_found', [ 'class' => get_class($model), 'id' => $recordId ])); } $result = $this->controller->formExtendModel($result) ?: $result; return $result; } /** * Creates a new instance of a form model. This logic can be changed * by overriding it in the controller. * @return \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model */ public function formCreateModelObject() { return $this->createModel(); } /** * Called before the form fields are defined. * @param \Backend\Widgets\Form $host The hosting form widget * @return void */ public function formExtendFieldsBefore($host) { } /** * Called after the form fields are defined. * @param \Backend\Widgets\Form $host The hosting form widget * @param array $fields Array of all defined form field objects (\Backend\Classes\FormField) * @return void */ public function formExtendFields($host, $fields) { } /** * Called before the form is refreshed, should return an array of additional save data. * @param \Backend\Widgets\Form $host The hosting form widget * @param array $saveData Current save data * @return array|void */ public function formExtendRefreshData($host, $saveData) { } /** * Called when the form is refreshed, giving the opportunity to modify the form fields. * @param \Backend\Widgets\Form $host The hosting form widget * @param array $fields Current form fields * @return array|void */ public function formExtendRefreshFields($host, $fields) { } /** * Called after the form is refreshed, should return an array of additional result parameters. * @param \Backend\Widgets\Form $host The hosting form widget * @param array $result Current result parameters. * @return array|void */ public function formExtendRefreshResults($host, $result) { } /** * Extend supplied model used by create and update actions, the model can * be altered by overriding it in the controller. * @param \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model $model * @return \Winter\Storm\Database\Model|\Winter\Storm\Halcyon\Model|void */ public function formExtendModel($model) { } /** * Extend the query used for finding the form model. Extra conditions * can be applied to the query, for example, $query->withTrashed(); * @param \Winter\Storm\Database\Builder|\Winter\Storm\Halcyon\Builder $query * @return void */ public function formExtendQuery($query) { } /** * Static helper for extending form fields. * @param callable $callback * @return void */ public static function extendFormFields($callback) { $calledClass = self::getCalledExtensionClass(); Event::listen('backend.form.extendFields', function ($widget) use ($calledClass, $callback) { if (!is_a($widget->getController(), $calledClass)) { return; } call_user_func_array($callback, [$widget, $widget->model, $widget->getContext()]); }); } /** * Controller accessor for making partials within this behavior. */ public function formMakePartial(string $partial, array $params = []): string { $contents = $this->controller->makePartial('form_' . $this->context . '_' . $partial, $params + $this->vars, false); if (!$contents) { $contents = $this->controller->makePartial('form_' . $partial, $params + $this->vars, false); } if (!$contents) { $contents = $this->makePartial($partial, $params); } return $contents; } }