mirror of
https://github.com/wintercms/winter.git
synced 2024-06-28 05:33:29 +02:00
Form fields are already constrained by their fields.yaml definition (values not defined in the yaml will not be saved) so we don't need to double dip by enforcing fillable too.
1422 lines
42 KiB
PHP
1422 lines
42 KiB
PHP
<?php namespace Backend\Behaviors;
|
|
|
|
use Db;
|
|
use Lang;
|
|
use Event;
|
|
use Request;
|
|
use Form as FormHelper;
|
|
use Backend\Classes\ControllerBehavior;
|
|
use October\Rain\Database\Model;
|
|
use ApplicationException;
|
|
|
|
/**
|
|
* Relation Controller Behavior
|
|
* Uses a combination of lists and forms for managing Model relations.
|
|
*
|
|
* @package october\backend
|
|
* @author Alexey Bobkov, Samuel Georges
|
|
*/
|
|
class RelationController extends ControllerBehavior
|
|
{
|
|
use \Backend\Traits\FormModelSaver;
|
|
|
|
/**
|
|
* @var const Postback parameter for the active relationship field.
|
|
*/
|
|
const PARAM_FIELD = '_relation_field';
|
|
|
|
/**
|
|
* @var const Postback parameter for the active management mode.
|
|
*/
|
|
const PARAM_MODE = '_relation_mode';
|
|
|
|
/**
|
|
* @var Backend\Classes\WidgetBase Reference to the search widget object.
|
|
*/
|
|
protected $searchWidget;
|
|
|
|
/**
|
|
* @var Backend\Classes\WidgetBase Reference to the toolbar widget object.
|
|
*/
|
|
protected $toolbarWidget;
|
|
|
|
/**
|
|
* @var Backend\Classes\WidgetBase Reference to the widget used for viewing (list or form).
|
|
*/
|
|
protected $viewWidget;
|
|
|
|
/**
|
|
* @var Backend\Classes\WidgetBase Reference to the widget used for relation management.
|
|
*/
|
|
protected $manageWidget;
|
|
|
|
/**
|
|
* @var Backend\Classes\WidgetBase Reference to widget for relations with pivot data.
|
|
*/
|
|
protected $pivotWidget;
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
protected $requiredProperties = ['relationConfig'];
|
|
|
|
/**
|
|
* @var array Properties that must exist for each relationship definition.
|
|
*/
|
|
protected $requiredRelationProperties = ['label'];
|
|
|
|
/**
|
|
* @var array Configuration values that must exist when applying the primary config file.
|
|
*/
|
|
protected $requiredConfig = [];
|
|
|
|
/**
|
|
* @var array Original configuration values
|
|
*/
|
|
protected $originalConfig;
|
|
|
|
/**
|
|
* @var bool Has the behavior been initialized.
|
|
*/
|
|
protected $initialized = false;
|
|
|
|
/**
|
|
* @var string Relationship type
|
|
*/
|
|
public $relationType;
|
|
|
|
/**
|
|
* @var string Relationship name
|
|
*/
|
|
public $relationName;
|
|
|
|
/**
|
|
* @var Model Relationship model
|
|
*/
|
|
public $relationModel;
|
|
|
|
/**
|
|
* @var Model Relationship object
|
|
*/
|
|
public $relationObject;
|
|
|
|
/**
|
|
* @var Model The parent model of the relationship.
|
|
*/
|
|
protected $model;
|
|
|
|
/**
|
|
* @var Model The relationship field as defined in the configuration.
|
|
*/
|
|
protected $field;
|
|
|
|
/**
|
|
* @var string A unique alias to pass to widgets.
|
|
*/
|
|
protected $alias;
|
|
|
|
/**
|
|
* @var array The set of buttons to display in view mode.
|
|
*/
|
|
protected $toolbarButtons;
|
|
|
|
/**
|
|
* @var Model Reference to the model used for viewing (form only).
|
|
*/
|
|
protected $viewModel;
|
|
|
|
/**
|
|
* @var string Relation has many (multi) or has one (single).
|
|
*/
|
|
protected $viewMode;
|
|
|
|
/**
|
|
* @var string Management of relation as list, form, or pivot.
|
|
*/
|
|
protected $manageMode;
|
|
|
|
/**
|
|
* @var string Force a certain view mode.
|
|
*/
|
|
protected $forceViewMode;
|
|
|
|
/**
|
|
* @var string Force a certain manage mode.
|
|
*/
|
|
protected $forceManageMode;
|
|
|
|
/**
|
|
* @var string The target that triggered an AJAX event (button, list)
|
|
*/
|
|
protected $eventTarget;
|
|
|
|
/**
|
|
* @var int Primary id of an existing relation record.
|
|
*/
|
|
protected $manageId;
|
|
|
|
/**
|
|
* @var int Foeign id of a selected pivot record.
|
|
*/
|
|
protected $foreignId;
|
|
|
|
/**
|
|
* @var string Active session key, used for deferred bindings.
|
|
*/
|
|
public $sessionKey;
|
|
|
|
/**
|
|
* @var bool Disables the ability to add, update, delete or create relations.
|
|
*/
|
|
public $readOnly = false;
|
|
|
|
/**
|
|
* @var bool Defers all binding actions using a session key when it is available.
|
|
*/
|
|
public $deferredBinding = false;
|
|
|
|
/**
|
|
* Behavior constructor
|
|
* @param Backend\Classes\Controller $controller
|
|
*/
|
|
public function __construct($controller)
|
|
{
|
|
parent::__construct($controller);
|
|
|
|
$this->addJs('js/october.relation.js', 'core');
|
|
$this->addCss('css/relation.css', 'core');
|
|
|
|
/*
|
|
* Build configuration
|
|
*/
|
|
$this->config = $this->originalConfig = $this->makeConfig($controller->relationConfig, $this->requiredConfig);
|
|
}
|
|
|
|
/**
|
|
* Validates the supplied field and initializes the relation manager.
|
|
* @param string $field The relationship field.
|
|
* @return string The active field name.
|
|
*/
|
|
protected function validateField($field = null)
|
|
{
|
|
$field = $field ?: post(self::PARAM_FIELD);
|
|
|
|
if ($field && $field != $this->field) {
|
|
$this->initRelation($this->model, $field);
|
|
}
|
|
|
|
if (!$field && !$this->field) {
|
|
throw new ApplicationException(Lang::get('backend::lang.relation.missing_definition', compact('field')));
|
|
}
|
|
|
|
return $field ?: $this->field;
|
|
}
|
|
|
|
/**
|
|
* Prepares the view data.
|
|
* @return void
|
|
*/
|
|
public function prepareVars()
|
|
{
|
|
$this->vars['relationManageId'] = $this->manageId;
|
|
$this->vars['relationLabel'] = $this->config->label ?: $this->field;
|
|
$this->vars['relationField'] = $this->field;
|
|
$this->vars['relationType'] = $this->relationType;
|
|
$this->vars['relationSearchWidget'] = $this->searchWidget;
|
|
$this->vars['relationToolbarWidget'] = $this->toolbarWidget;
|
|
$this->vars['relationManageMode'] = $this->manageMode;
|
|
$this->vars['relationManageWidget'] = $this->manageWidget;
|
|
$this->vars['relationToolbarButtons'] = $this->toolbarButtons;
|
|
$this->vars['relationViewMode'] = $this->viewMode;
|
|
$this->vars['relationViewWidget'] = $this->viewWidget;
|
|
$this->vars['relationViewModel'] = $this->viewModel;
|
|
$this->vars['relationPivotWidget'] = $this->pivotWidget;
|
|
$this->vars['relationSessionKey'] = $this->relationGetSessionKey();
|
|
}
|
|
|
|
/**
|
|
* The controller action is responsible for supplying the parent model
|
|
* so it's action must be fired. Additionally, each AJAX request must
|
|
* supply the relation's field name (_relation_field).
|
|
*/
|
|
protected function beforeAjax()
|
|
{
|
|
if ($this->initialized) {
|
|
return;
|
|
}
|
|
|
|
$this->controller->pageAction();
|
|
$this->validateField();
|
|
$this->prepareVars();
|
|
$this->initialized = true;
|
|
}
|
|
|
|
//
|
|
// Interface
|
|
//
|
|
|
|
/**
|
|
* Prepare the widgets used by this behavior
|
|
* @param Model $model
|
|
* @param string $field
|
|
* @return void
|
|
*/
|
|
public function initRelation($model, $field = null)
|
|
{
|
|
if ($field == null) {
|
|
$field = post(self::PARAM_FIELD);
|
|
}
|
|
|
|
$this->config = $this->originalConfig;
|
|
$this->model = $model;
|
|
$this->field = $field;
|
|
|
|
if ($field == null) {
|
|
return;
|
|
}
|
|
|
|
if (!$this->model) {
|
|
throw new ApplicationException(Lang::get(
|
|
'backend::lang.relation.missing_model',
|
|
['class'=>get_class($this->controller)]
|
|
));
|
|
}
|
|
|
|
if (!$this->model instanceof Model) {
|
|
throw new ApplicationException(Lang::get(
|
|
'backend::lang.model.invalid_class',
|
|
['model'=>get_class($this->model), 'class'=>get_class($this->controller)]
|
|
));
|
|
}
|
|
|
|
if (!$this->getConfig($field)) {
|
|
throw new ApplicationException(Lang::get('backend::lang.relation.missing_definition', compact('field')));
|
|
}
|
|
|
|
$this->alias = camel_case('relation ' . $field);
|
|
$this->config = $this->makeConfig($this->getConfig($field), $this->requiredRelationProperties);
|
|
|
|
/*
|
|
* Relationship details
|
|
*/
|
|
$this->relationName = $field;
|
|
$this->relationType = $this->model->getRelationType($field);
|
|
$this->relationObject = $this->model->{$field}();
|
|
$this->relationModel = $this->relationObject->getRelated();
|
|
|
|
$this->readOnly = $this->getConfig('readOnly');
|
|
$this->deferredBinding = $this->getConfig('deferredBinding') || !$this->model->exists;
|
|
$this->toolbarButtons = $this->evalToolbarButtons();
|
|
$this->viewMode = $this->evalViewMode();
|
|
$this->manageMode = $this->evalManageMode();
|
|
$this->manageId = post('manage_id');
|
|
$this->foreignId = post('foreign_id');
|
|
|
|
/*
|
|
* Toolbar widget
|
|
*/
|
|
if ($this->toolbarWidget = $this->makeToolbarWidget()) {
|
|
$this->toolbarWidget->bindToController();
|
|
}
|
|
|
|
/*
|
|
* Search widget
|
|
*/
|
|
if ($this->searchWidget = $this->makeSearchWidget()) {
|
|
$this->searchWidget->bindToController();
|
|
}
|
|
|
|
/*
|
|
* View widget
|
|
*/
|
|
if ($this->viewWidget = $this->makeViewWidget()) {
|
|
$this->controller->relationExtendViewWidget($this->viewWidget, $this->field);
|
|
$this->viewWidget->bindToController();
|
|
}
|
|
|
|
/*
|
|
* Manage widget
|
|
*/
|
|
if ($this->manageWidget = $this->makeManageWidget()) {
|
|
$this->controller->relationExtendManageWidget($this->manageWidget, $this->field);
|
|
$this->manageWidget->bindToController();
|
|
}
|
|
|
|
/*
|
|
* Pivot widget
|
|
*/
|
|
if ($this->manageMode == 'pivot') {
|
|
if ($this->pivotWidget = $this->makePivotWidget()) {
|
|
$this->controller->relationExtendPivotWidget($this->pivotWidget, $this->field);
|
|
$this->pivotWidget->bindToController();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Renders the relationship manager.
|
|
* @param string $field The relationship field.
|
|
* @param array $options
|
|
* @return string Rendered HTML for the relationship manager.
|
|
*/
|
|
public function relationRender($field, $options = [])
|
|
{
|
|
$field = $this->validateField($field);
|
|
|
|
if (is_string($options)) {
|
|
$options = ['sessionKey' => $options];
|
|
}
|
|
|
|
$this->prepareVars();
|
|
|
|
/*
|
|
* Session key
|
|
*/
|
|
if (isset($options['sessionKey'])) {
|
|
$this->sessionKey = $options['sessionKey'];
|
|
}
|
|
|
|
/*
|
|
* Determine the partial to use based on the supplied section option
|
|
*/
|
|
$section = (isset($options['section'])) ? $options['section'] : null;
|
|
switch (strtolower($section)) {
|
|
case 'toolbar':
|
|
return $this->toolbarWidget ? $this->toolbarWidget->render() : null;
|
|
|
|
case 'view':
|
|
return $this->relationMakePartial('view');
|
|
|
|
default:
|
|
return $this->relationMakePartial('container');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Refreshes the relation container only, useful for returning in custom AJAX requests.
|
|
* @param string $field Relation definition.
|
|
* @return array The relation element selector as the key, and the relation view contents are the value.
|
|
*/
|
|
public function relationRefresh($field = null)
|
|
{
|
|
$field = $this->validateField($field);
|
|
|
|
$result = ['#'.$this->relationGetId('view') => $this->relationRenderView($field)];
|
|
if ($toolbar = $this->relationRenderToolbar($field)) {
|
|
$result['#'.$this->relationGetId('toolbar')] = $toolbar;
|
|
}
|
|
|
|
if ($eventResult = $this->controller->relationExtendRefreshResults($field)) {
|
|
$result = $eventResult + $result;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Renders the toolbar only.
|
|
* @param string $field The relationship field.
|
|
* @return string Rendered HTML for the toolbar.
|
|
*/
|
|
public function relationRenderToolbar($field = null)
|
|
{
|
|
return $this->relationRender($field, ['section' => 'toolbar']);
|
|
}
|
|
|
|
/**
|
|
* Renders the view only.
|
|
* @param string $field The relationship field.
|
|
* @return string Rendered HTML for the view.
|
|
*/
|
|
public function relationRenderView($field = null)
|
|
{
|
|
return $this->relationRender($field, ['section' => 'view']);
|
|
}
|
|
|
|
/**
|
|
* Controller accessor for making partials within this behavior.
|
|
* @param string $partial
|
|
* @param array $params
|
|
* @return string Partial contents
|
|
*/
|
|
public function relationMakePartial($partial, $params = [])
|
|
{
|
|
$contents = $this->controller->makePartial('relation_'.$partial, $params + $this->vars, false);
|
|
if (!$contents) {
|
|
$contents = $this->makePartial($partial, $params);
|
|
}
|
|
|
|
return $contents;
|
|
}
|
|
|
|
/**
|
|
* Returns a unique ID for this relation and field combination.
|
|
* @param string $suffix A suffix to use with the identifier.
|
|
* @return string
|
|
*/
|
|
public function relationGetId($suffix = null)
|
|
{
|
|
$id = class_basename($this);
|
|
if ($this->field) {
|
|
$id .= '-' . $this->field;
|
|
}
|
|
|
|
if ($suffix !== null) {
|
|
$id .= '-' . $suffix;
|
|
}
|
|
|
|
return $this->controller->getId($id);
|
|
}
|
|
|
|
/**
|
|
* Returns the active session key.
|
|
*/
|
|
public function relationGetSessionKey($force = false)
|
|
{
|
|
if ($this->sessionKey && !$force) {
|
|
return $this->sessionKey;
|
|
}
|
|
|
|
if (post('_relation_session_key')) {
|
|
return $this->sessionKey = post('_relation_session_key');
|
|
}
|
|
|
|
if (post('_session_key')) {
|
|
return $this->sessionKey = post('_session_key');
|
|
}
|
|
|
|
return $this->sessionKey = FormHelper::getSessionKey();
|
|
}
|
|
|
|
//
|
|
// Widgets
|
|
//
|
|
|
|
protected function makeToolbarWidget()
|
|
{
|
|
$defaultConfig = [];
|
|
|
|
/*
|
|
* Add buttons to toolbar
|
|
*/
|
|
$defaultButtons = null;
|
|
|
|
if (!$this->readOnly) {
|
|
$defaultButtons = '~/modules/backend/behaviors/relationcontroller/partials/_toolbar.htm';
|
|
}
|
|
|
|
$defaultConfig['buttons'] = $this->getConfig('view[toolbarPartial]', $defaultButtons);
|
|
|
|
/*
|
|
* Make config
|
|
*/
|
|
$toolbarConfig = $this->makeConfig($this->getConfig('toolbar', $defaultConfig));
|
|
$toolbarConfig->alias = $this->alias . 'Toolbar';
|
|
|
|
/*
|
|
* Add search to toolbar
|
|
*/
|
|
$useSearch = $this->viewMode == 'multi' && $this->getConfig('view[showSearch]');
|
|
|
|
if ($useSearch) {
|
|
$toolbarConfig->search = [
|
|
'prompt' => 'backend::lang.list.search_prompt'
|
|
];
|
|
}
|
|
|
|
/*
|
|
* No buttons, no search should mean no toolbar
|
|
*/
|
|
if (empty($toolbarConfig->search) && empty($toolbarConfig->buttons))
|
|
return;
|
|
|
|
$toolbarWidget = $this->makeWidget('Backend\Widgets\Toolbar', $toolbarConfig);
|
|
$toolbarWidget->cssClasses[] = 'list-header';
|
|
|
|
return $toolbarWidget;
|
|
}
|
|
|
|
protected function makeSearchWidget()
|
|
{
|
|
if (!$this->getConfig('manage[showSearch]')) {
|
|
return null;
|
|
}
|
|
|
|
$config = $this->makeConfig();
|
|
$config->alias = $this->alias . 'ManageSearch';
|
|
$config->growable = false;
|
|
$config->prompt = 'backend::lang.list.search_prompt';
|
|
$widget = $this->makeWidget('Backend\Widgets\Search', $config);
|
|
$widget->cssClasses[] = 'recordfinder-search';
|
|
|
|
/*
|
|
* Persist the search term across AJAX requests only
|
|
*/
|
|
if (!Request::ajax()) {
|
|
$widget->setActiveTerm(null);
|
|
}
|
|
|
|
return $widget;
|
|
}
|
|
|
|
protected function makeViewWidget()
|
|
{
|
|
/*
|
|
* Multiple (has many, belongs to many)
|
|
*/
|
|
if ($this->viewMode == 'multi') {
|
|
$config = $this->makeConfigForMode('view', 'list');
|
|
$config->model = $this->relationModel;
|
|
$config->alias = $this->alias . 'ViewList';
|
|
$config->showSorting = $this->getConfig('view[showSorting]', true);
|
|
$config->defaultSort = $this->getConfig('view[defaultSort]');
|
|
$config->recordsPerPage = $this->getConfig('view[recordsPerPage]');
|
|
$config->showCheckboxes = $this->getConfig('view[showCheckboxes]', !$this->readOnly);
|
|
$config->recordUrl = $this->getConfig('view[recordUrl]', null);
|
|
|
|
$defaultOnClick = sprintf(
|
|
"$.oc.relationBehavior.clickViewListRecord(':%s', '%s', '%s')",
|
|
$this->relationModel->getKeyName(),
|
|
$this->field,
|
|
$this->relationGetSessionKey()
|
|
);
|
|
|
|
if ($config->recordUrl) {
|
|
$defaultOnClick = null;
|
|
}
|
|
elseif (
|
|
!$this->makeConfigForMode('manage', 'form', false) &&
|
|
!$this->makeConfigForMode('pivot', 'form', false)
|
|
) {
|
|
$defaultOnClick = null;
|
|
}
|
|
|
|
$config->recordOnClick = $this->getConfig('view[recordOnClick]', $defaultOnClick);
|
|
|
|
if ($emptyMessage = $this->getConfig('emptyMessage')) {
|
|
$config->noRecordsMessage = $emptyMessage;
|
|
}
|
|
|
|
/*
|
|
* Constrain the query by the relationship and deferred items
|
|
*/
|
|
$widget = $this->makeWidget('Backend\Widgets\Lists', $config);
|
|
$widget->bindEvent('list.extendQuery', function ($query) {
|
|
$this->relationObject->setQuery($query);
|
|
|
|
$sessionKey = $this->deferredBinding ? $this->relationGetSessionKey() : null;
|
|
|
|
if ($sessionKey) {
|
|
$this->relationObject->withDeferred($sessionKey);
|
|
}
|
|
elseif ($this->model->exists) {
|
|
$this->relationObject->addConstraints();
|
|
}
|
|
|
|
$this->controller->relationExtendQuery($query, $this->field);
|
|
|
|
/*
|
|
* Allows pivot data to enter the fray
|
|
*/
|
|
if ($this->relationType == 'belongsToMany') {
|
|
$this->relationObject->setQuery($query->getQuery());
|
|
return $this->relationObject;
|
|
}
|
|
});
|
|
|
|
/*
|
|
* Constrain the list by the search widget, if available
|
|
*/
|
|
if ($this->toolbarWidget && $this->getConfig('view[showSearch]')) {
|
|
if ($searchWidget = $this->toolbarWidget->getSearchWidget()) {
|
|
$searchWidget->bindEvent('search.submit', function () use ($widget, $searchWidget) {
|
|
$widget->setSearchTerm($searchWidget->getActiveTerm());
|
|
return $widget->onRefresh();
|
|
});
|
|
|
|
/*
|
|
* Persist the search term across AJAX requests only
|
|
*/
|
|
if (Request::ajax()) {
|
|
$widget->setSearchTerm($searchWidget->getActiveTerm());
|
|
}
|
|
else {
|
|
$searchWidget->setActiveTerm(null);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
* Single (belongs to, has one)
|
|
*/
|
|
elseif ($this->viewMode == 'single') {
|
|
$this->viewModel = $this->relationObject->getResults()
|
|
?: $this->relationModel;
|
|
|
|
$config = $this->makeConfigForMode('view', 'form');
|
|
$config->model = $this->viewModel;
|
|
$config->arrayName = class_basename($this->relationModel);
|
|
$config->context = 'relation';
|
|
$config->alias = $this->alias . 'ViewForm';
|
|
|
|
$widget = $this->makeWidget('Backend\Widgets\Form', $config);
|
|
$widget->previewMode = true;
|
|
}
|
|
|
|
return $widget;
|
|
}
|
|
|
|
protected function makeManageWidget()
|
|
{
|
|
$widget = null;
|
|
|
|
/*
|
|
* List / Pivot
|
|
*/
|
|
if ($this->manageMode == 'list' || $this->manageMode == 'pivot') {
|
|
$isPivot = $this->manageMode == 'pivot';
|
|
|
|
$config = $this->makeConfigForMode('manage', 'list');
|
|
$config->model = $this->relationModel;
|
|
$config->alias = $this->alias . 'ManageList';
|
|
$config->showSetup = false;
|
|
$config->showCheckboxes = $this->getConfig('manage[showCheckboxes]', !$isPivot);
|
|
$config->showSorting = $this->getConfig('manage[showSorting]', !$isPivot);
|
|
$config->defaultSort = $this->getConfig('manage[defaultSort]');
|
|
$config->recordsPerPage = $this->getConfig('manage[recordsPerPage]');
|
|
|
|
if ($this->viewMode == 'single') {
|
|
$config->showCheckboxes = false;
|
|
$config->recordOnClick = sprintf(
|
|
"$.oc.relationBehavior.clickManageListRecord(:id, '%s', '%s')",
|
|
$this->field,
|
|
$this->relationGetSessionKey()
|
|
);
|
|
}
|
|
elseif ($config->showCheckboxes) {
|
|
$config->recordOnClick = "$.oc.relationBehavior.toggleListCheckbox(this)";
|
|
}
|
|
elseif ($isPivot) {
|
|
$config->recordOnClick = sprintf(
|
|
"$.oc.relationBehavior.clickManagePivotListRecord(:id, '%s', '%s')",
|
|
$this->field,
|
|
$this->relationGetSessionKey()
|
|
);
|
|
}
|
|
|
|
$widget = $this->makeWidget('Backend\Widgets\Lists', $config);
|
|
|
|
/*
|
|
* Link the Search Widget to the List Widget
|
|
*/
|
|
if ($this->searchWidget) {
|
|
$this->searchWidget->bindEvent('search.submit', function () use ($widget) {
|
|
$widget->setSearchTerm($this->searchWidget->getActiveTerm());
|
|
return $widget->onRefresh();
|
|
});
|
|
|
|
/*
|
|
* Persist the search term across AJAX requests only
|
|
*/
|
|
if (Request::ajax()) {
|
|
$widget->setSearchTerm($this->searchWidget->getActiveTerm());
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
* Form
|
|
*/
|
|
elseif ($this->manageMode == 'form') {
|
|
|
|
$config = $this->makeConfigForMode('manage', 'form');
|
|
$config->model = $this->relationModel;
|
|
$config->arrayName = class_basename($this->relationModel);
|
|
$config->context = $this->evalFormContext('manage', !!$this->manageId);
|
|
$config->alias = $this->alias . 'ManageForm';
|
|
|
|
/*
|
|
* Existing record
|
|
*/
|
|
if ($this->manageId) {
|
|
$config->model = $config->model->find($this->manageId);
|
|
if (!$config->model) {
|
|
throw new ApplicationException(Lang::get('backend::lang.model.not_found', [
|
|
'class' => get_class($config->model), 'id' => $this->manageId
|
|
]));
|
|
}
|
|
}
|
|
|
|
$widget = $this->makeWidget('Backend\Widgets\Form', $config);
|
|
}
|
|
|
|
if (!$widget) {
|
|
return null;
|
|
}
|
|
|
|
/*
|
|
* Exclude existing relationships
|
|
*/
|
|
if ($this->manageMode == 'pivot' || $this->manageMode == 'list') {
|
|
$widget->bindEvent('list.extendQuery', function ($query) {
|
|
/*
|
|
* Where not in the current list of related records
|
|
*/
|
|
$existingIds = $this->findExistingRelationIds();
|
|
if (count($existingIds)) {
|
|
$query->whereNotIn($this->relationModel->getQualifiedKeyName(), $existingIds);
|
|
}
|
|
|
|
$this->controller->relationExtendQuery($query, $this->field);
|
|
});
|
|
}
|
|
|
|
return $widget;
|
|
}
|
|
|
|
protected function makePivotWidget()
|
|
{
|
|
$config = $this->makeConfigForMode('pivot', 'form');
|
|
$config->model = $this->relationModel;
|
|
$config->arrayName = class_basename($this->relationModel);
|
|
$config->context = $this->evalFormContext('pivot', !!$this->manageId);
|
|
$config->alias = $this->alias . 'ManagePivotForm';
|
|
|
|
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
|
|
|
|
/*
|
|
* Existing record
|
|
*/
|
|
if ($this->manageId) {
|
|
$hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first();
|
|
|
|
$config->model = $hydratedModel;
|
|
if (!$config->model) {
|
|
throw new ApplicationException(Lang::get('backend::lang.model.not_found', [
|
|
'class' => get_class($config->model), 'id' => $this->manageId
|
|
]));
|
|
}
|
|
}
|
|
/*
|
|
* New record
|
|
*/
|
|
else {
|
|
if ($this->foreignId) {
|
|
$foreignModel = $this->relationModel
|
|
->whereIn($foreignKeyName, (array) $this->foreignId)
|
|
->first();
|
|
|
|
if ($foreignModel) {
|
|
$foreignModel->exists = false;
|
|
$config->model = $foreignModel;
|
|
}
|
|
}
|
|
|
|
$pivotModel = $this->relationObject->newPivot();
|
|
$config->model->setRelation('pivot', $pivotModel);
|
|
}
|
|
|
|
$widget = $this->makeWidget('Backend\Widgets\Form', $config);
|
|
return $widget;
|
|
}
|
|
|
|
//
|
|
// AJAX (Buttons)
|
|
//
|
|
|
|
public function onRelationButtonAdd()
|
|
{
|
|
$this->eventTarget = 'button-add';
|
|
return $this->onRelationManageForm();
|
|
}
|
|
|
|
public function onRelationButtonCreate()
|
|
{
|
|
$this->eventTarget = 'button-create';
|
|
return $this->onRelationManageForm();
|
|
}
|
|
|
|
public function onRelationButtonDelete()
|
|
{
|
|
return $this->onRelationManageDelete();
|
|
}
|
|
|
|
public function onRelationButtonLink()
|
|
{
|
|
$this->eventTarget = 'button-link';
|
|
return $this->onRelationManageForm();
|
|
}
|
|
|
|
public function onRelationButtonUnlink()
|
|
{
|
|
return $this->onRelationManageRemove();
|
|
}
|
|
|
|
public function onRelationButtonRemove()
|
|
{
|
|
return $this->onRelationManageRemove();
|
|
}
|
|
|
|
public function onRelationButtonUpdate()
|
|
{
|
|
$this->eventTarget = 'button-update';
|
|
return $this->onRelationManageForm();
|
|
}
|
|
|
|
//
|
|
// AJAX (List events)
|
|
//
|
|
|
|
public function onRelationClickManageList()
|
|
{
|
|
return $this->onRelationManageAdd();
|
|
}
|
|
|
|
public function onRelationClickManageListPivot()
|
|
{
|
|
return $this->onRelationManagePivotForm();
|
|
}
|
|
|
|
public function onRelationClickViewList()
|
|
{
|
|
$this->eventTarget = 'list';
|
|
return $this->onRelationManageForm();
|
|
}
|
|
|
|
//
|
|
// AJAX
|
|
//
|
|
|
|
public function onRelationManageForm()
|
|
{
|
|
$this->beforeAjax();
|
|
|
|
if ($this->manageMode == 'pivot' && $this->manageId) {
|
|
return $this->onRelationManagePivotForm();
|
|
}
|
|
|
|
// The form should not share its session key with the parent
|
|
$this->vars['newSessionKey'] = str_random(40);
|
|
|
|
$view = 'manage_' . $this->manageMode;
|
|
return $this->relationMakePartial($view);
|
|
}
|
|
|
|
/**
|
|
* Create a new related model
|
|
*/
|
|
public function onRelationManageCreate()
|
|
{
|
|
$this->forceManageMode = 'form';
|
|
$this->beforeAjax();
|
|
$saveData = $this->manageWidget->getSaveData();
|
|
$sessionKey = $this->deferredBinding ? $this->relationGetSessionKey(true) : null;
|
|
|
|
if ($this->viewMode == 'multi') {
|
|
$newModel = $this->relationModel;
|
|
$modelsToSave = $this->prepareModelsToSave($newModel, $saveData);
|
|
foreach ($modelsToSave as $modelToSave) {
|
|
$modelToSave->save(null, $this->manageWidget->getSessionKey());
|
|
}
|
|
|
|
$this->relationObject->add($newModel, $sessionKey);
|
|
}
|
|
elseif ($this->viewMode == 'single') {
|
|
$newModel = $this->viewModel;
|
|
$this->viewWidget->setFormValues($saveData);
|
|
|
|
/*
|
|
* Has one relations will save as part of the add() call.
|
|
*/
|
|
if ($this->deferredBinding || $this->relationType != 'hasOne') {
|
|
$newModel->save();
|
|
}
|
|
|
|
$this->relationObject->add($newModel, $sessionKey);
|
|
|
|
/*
|
|
* Belongs to relations won't save when using add() so
|
|
* it should occur if the conditions are right.
|
|
*/
|
|
if (!$this->deferredBinding && $this->relationType == 'belongsTo') {
|
|
$parentModel = $this->relationObject->getParent();
|
|
if ($parentModel->exists) {
|
|
$parentModel->save();
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->relationRefresh();
|
|
}
|
|
|
|
/**
|
|
* Updated an existing related model's fields
|
|
*/
|
|
public function onRelationManageUpdate()
|
|
{
|
|
$this->forceManageMode = 'form';
|
|
$this->beforeAjax();
|
|
$saveData = $this->manageWidget->getSaveData();
|
|
|
|
if ($this->viewMode == 'multi') {
|
|
$model = $this->relationModel->find($this->manageId);
|
|
$modelsToSave = $this->prepareModelsToSave($model, $saveData);
|
|
foreach ($modelsToSave as $modelToSave) {
|
|
$modelToSave->save(null, $this->manageWidget->getSessionKey());
|
|
}
|
|
}
|
|
elseif ($this->viewMode == 'single') {
|
|
$this->viewWidget->setFormValues($saveData);
|
|
$this->viewModel->save();
|
|
}
|
|
|
|
return ['#'.$this->relationGetId('view') => $this->relationRenderView()];
|
|
}
|
|
|
|
/**
|
|
* Delete an existing related model completely
|
|
*/
|
|
public function onRelationManageDelete()
|
|
{
|
|
$this->beforeAjax();
|
|
|
|
/*
|
|
* Multiple (has many, belongs to many)
|
|
*/
|
|
if ($this->viewMode == 'multi') {
|
|
if (($checkedIds = post('checked')) && is_array($checkedIds)) {
|
|
$relatedModel = $this->relationObject->getRelated();
|
|
foreach ($checkedIds as $relationId) {
|
|
if (!$obj = $relatedModel->find($relationId)) {
|
|
continue;
|
|
}
|
|
|
|
$obj->delete();
|
|
}
|
|
}
|
|
}
|
|
/*
|
|
* Single (belongs to, has one)
|
|
*/
|
|
elseif ($this->viewMode == 'single') {
|
|
$relatedModel = $this->viewModel;
|
|
if ($relatedModel->exists) {
|
|
$relatedModel->delete();
|
|
}
|
|
|
|
$this->viewWidget->setFormValues([]);
|
|
$this->viewModel = $this->relationModel;
|
|
}
|
|
|
|
return $this->relationRefresh();
|
|
}
|
|
|
|
/**
|
|
* Add an existing related model to the primary model
|
|
*/
|
|
public function onRelationManageAdd()
|
|
{
|
|
$this->beforeAjax();
|
|
|
|
$recordId = post('record_id');
|
|
$sessionKey = $this->deferredBinding ? $this->relationGetSessionKey() : null;
|
|
|
|
/*
|
|
* Add
|
|
*/
|
|
if ($this->viewMode == 'multi') {
|
|
|
|
$checkedIds = $recordId ? [$recordId] : post('checked');
|
|
|
|
if (is_array($checkedIds)) {
|
|
/*
|
|
* Remove existing relations from the array
|
|
*/
|
|
$existingIds = $this->findExistingRelationIds($checkedIds);
|
|
$checkedIds = array_diff($checkedIds, $existingIds);
|
|
$foreignKeyName = $this->relationModel->getKeyName();
|
|
|
|
$models = $this->relationModel->whereIn($foreignKeyName, $checkedIds)->get();
|
|
foreach ($models as $model) {
|
|
$this->relationObject->add($model, $sessionKey);
|
|
}
|
|
}
|
|
|
|
}
|
|
/*
|
|
* Link
|
|
*/
|
|
elseif ($this->viewMode == 'single') {
|
|
if ($recordId && ($model = $this->relationModel->find($recordId))) {
|
|
|
|
$this->relationObject->add($model, $sessionKey);
|
|
$this->viewWidget->setFormValues($model->attributes);
|
|
|
|
/*
|
|
* Belongs to relations won't save when using add() so
|
|
* it should occur if the conditions are right.
|
|
*/
|
|
if (!$this->deferredBinding && $this->relationType == 'belongsTo') {
|
|
$parentModel = $this->relationObject->getParent();
|
|
if ($parentModel->exists) {
|
|
$parentModel->save();
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return $this->relationRefresh();
|
|
}
|
|
|
|
/**
|
|
* Remove an existing related model from the primary model
|
|
*/
|
|
public function onRelationManageRemove()
|
|
{
|
|
$this->beforeAjax();
|
|
|
|
$recordId = post('record_id');
|
|
|
|
/*
|
|
* Remove
|
|
*/
|
|
if ($this->viewMode == 'multi') {
|
|
|
|
$checkedIds = $recordId ? [$recordId] : post('checked');
|
|
|
|
if (is_array($checkedIds)) {
|
|
|
|
if ($this->relationType == 'belongsToMany') {
|
|
$this->relationObject->detach($checkedIds);
|
|
}
|
|
elseif ($this->relationType == 'hasMany') {
|
|
$relatedModel = $this->relationObject->getRelated();
|
|
foreach ($checkedIds as $relationId) {
|
|
if ($obj = $relatedModel->find($relationId)) {
|
|
$this->relationObject->remove($obj);
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
/*
|
|
* Unlink
|
|
*/
|
|
elseif ($this->viewMode == 'single') {
|
|
if ($this->relationType == 'belongsTo') {
|
|
$this->relationObject->dissociate();
|
|
$this->relationObject->getParent()->save();
|
|
}
|
|
elseif ($this->relationType == 'hasOne') {
|
|
if ($obj = $this->relationModel->find($recordId)) {
|
|
$this->relationObject->remove($obj);
|
|
}
|
|
elseif ($this->viewModel->exists) {
|
|
$this->relationObject->remove($this->viewModel);
|
|
}
|
|
}
|
|
|
|
$this->viewWidget->setFormValues([]);
|
|
}
|
|
|
|
return $this->relationRefresh();
|
|
}
|
|
|
|
/**
|
|
* Add multiple items using a single pivot form.
|
|
*/
|
|
public function onRelationManageAddPivot()
|
|
{
|
|
return $this->onRelationManagePivotForm();
|
|
}
|
|
|
|
public function onRelationManagePivotForm()
|
|
{
|
|
$this->beforeAjax();
|
|
|
|
$this->vars['foreignId'] = $this->foreignId ?: post('checked');
|
|
return $this->relationMakePartial('pivot_form');
|
|
}
|
|
|
|
public function onRelationManagePivotCreate()
|
|
{
|
|
$this->beforeAjax();
|
|
|
|
/*
|
|
* If the pivot model fails for some reason, abort the sync
|
|
*/
|
|
Db::transaction(function () {
|
|
/*
|
|
* Add the checked IDs to the pivot table
|
|
*/
|
|
$foreignIds = (array) $this->foreignId;
|
|
$this->relationObject->sync($foreignIds, false);
|
|
|
|
/*
|
|
* Save data to models
|
|
*/
|
|
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
|
|
$hyrdatedModels = $this->relationObject->whereIn($foreignKeyName, $foreignIds)->get();
|
|
$saveData = $this->pivotWidget->getSaveData();
|
|
|
|
foreach ($hyrdatedModels as $hydratedModel) {
|
|
$modelsToSave = $this->prepareModelsToSave($hydratedModel, $saveData);
|
|
foreach ($modelsToSave as $modelToSave) {
|
|
$modelToSave->save();
|
|
}
|
|
}
|
|
});
|
|
|
|
return ['#'.$this->relationGetId('view') => $this->relationRenderView()];
|
|
}
|
|
|
|
public function onRelationManagePivotUpdate()
|
|
{
|
|
$this->beforeAjax();
|
|
|
|
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
|
|
$hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first();
|
|
$saveData = $this->pivotWidget->getSaveData();
|
|
|
|
$modelsToSave = $this->prepareModelsToSave($hydratedModel, $saveData);
|
|
foreach ($modelsToSave as $modelToSave) {
|
|
$modelToSave->save();
|
|
}
|
|
|
|
return ['#'.$this->relationGetId('view') => $this->relationRenderView()];
|
|
}
|
|
|
|
//
|
|
// Overrides
|
|
//
|
|
|
|
/**
|
|
* !!!!
|
|
* !!!! WARNING: DO NOT USE - This method is scheduled to be removed
|
|
* !!!!
|
|
*
|
|
* Controller override: Extend the query used for populating the list
|
|
* after the default query is processed.
|
|
* @param October\Rain\Database\Builder $query
|
|
* @param string $field
|
|
*/
|
|
public function relationExtendQuery($query, $field)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Provides an opportunity to manipulate the view widget.
|
|
* @param Backend\Classes\WidgetBase $widget
|
|
* @param string $field
|
|
*/
|
|
public function relationExtendViewWidget($widget, $field)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Provides an opportunity to manipulate the manage widget.
|
|
* @param Backend\Classes\WidgetBase $widget
|
|
* @param string $field
|
|
*/
|
|
public function relationExtendManageWidget($widget, $field)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* Provides an opportunity to manipulate the pivot widget.
|
|
* @param Backend\Classes\WidgetBase $widget
|
|
* @param string $field
|
|
*/
|
|
public function relationExtendPivotWidget($widget, $field)
|
|
{
|
|
}
|
|
|
|
/**
|
|
* The view widget is often refreshed when the manage widget makes a change,
|
|
* you can use this method to inject additional containers when this process
|
|
* occurs. Return an array with the extra values to send to the browser, eg:
|
|
*
|
|
* return ['#myCounter' => 'Total records: 6'];
|
|
*
|
|
* @param string $field
|
|
* @return array
|
|
*/
|
|
public function relationExtendRefreshResults($field)
|
|
{
|
|
}
|
|
|
|
//
|
|
// Helpers
|
|
//
|
|
|
|
/**
|
|
* Returns the existing record IDs for the relation.
|
|
*/
|
|
protected function findExistingRelationIds($checkIds = null)
|
|
{
|
|
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
|
|
|
|
$results = $this->relationObject
|
|
->getBaseQuery()
|
|
->select($foreignKeyName);
|
|
|
|
if ($checkIds !== null && is_array($checkIds) && count($checkIds)) {
|
|
$results = $results->whereIn($foreignKeyName, $checkIds);
|
|
}
|
|
|
|
return $results->lists($foreignKeyName);
|
|
}
|
|
|
|
/**
|
|
* Determine the default buttons based on the model relationship type.
|
|
* @return string
|
|
*/
|
|
protected function evalToolbarButtons()
|
|
{
|
|
if ($buttons = $this->getConfig('view[toolbarButtons]')) {
|
|
return is_array($buttons)
|
|
? $buttons
|
|
: array_map('trim', explode('|', $buttons));
|
|
}
|
|
|
|
switch ($this->relationType) {
|
|
case 'hasMany':
|
|
case 'belongsToMany':
|
|
return ['create', 'add', 'delete', 'remove'];
|
|
|
|
case 'hasOne':
|
|
case 'belongsTo':
|
|
return ['create', 'update', 'link', 'delete', 'unlink'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine the view mode based on the model relationship type.
|
|
* @return string
|
|
*/
|
|
protected function evalViewMode()
|
|
{
|
|
if ($this->forceViewMode) {
|
|
return $this->forceViewMode;
|
|
}
|
|
|
|
switch ($this->relationType) {
|
|
case 'hasMany':
|
|
case 'belongsToMany':
|
|
return 'multi';
|
|
|
|
case 'hasOne':
|
|
case 'belongsTo':
|
|
return 'single';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine the management mode based on the relation type and settings.
|
|
* @return string
|
|
*/
|
|
protected function evalManageMode()
|
|
{
|
|
if ($mode = post(self::PARAM_MODE)) {
|
|
return $mode;
|
|
}
|
|
|
|
if ($this->forceManageMode) {
|
|
return $this->forceManageMode;
|
|
}
|
|
|
|
switch ($this->eventTarget) {
|
|
case 'button-create':
|
|
case 'button-update':
|
|
return 'form';
|
|
|
|
case 'button-link':
|
|
return 'list';
|
|
}
|
|
|
|
switch ($this->relationType) {
|
|
case 'belongsTo':
|
|
return 'list';
|
|
|
|
case 'belongsToMany':
|
|
if (isset($this->config->pivot)) return 'pivot';
|
|
elseif ($this->eventTarget == 'list') return 'form';
|
|
else return 'list';
|
|
|
|
case 'hasOne':
|
|
case 'hasMany':
|
|
if ($this->eventTarget == 'button-add') return 'list';
|
|
else return 'form';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determine supplied form context.
|
|
*/
|
|
protected function evalFormContext($mode = 'manage', $exists = false)
|
|
{
|
|
$config = isset($this->config->{$mode}) ? $this->config->{$mode} : [];
|
|
|
|
if ($context = array_get($config, 'context')) {
|
|
if (is_array($context)) {
|
|
$context = $exists
|
|
? array_get($context, 'update')
|
|
: array_get($context, 'create');
|
|
}
|
|
}
|
|
|
|
if (!$context) {
|
|
$context = $exists ? 'update' : 'create';
|
|
}
|
|
|
|
return $context;
|
|
}
|
|
|
|
/**
|
|
* Returns the configuration for a mode (view, manage, pivot) for an
|
|
* expected type (list, form). Uses fallback configuration.
|
|
*/
|
|
protected function makeConfigForMode($mode = 'view', $type = 'list', $throwException = true)
|
|
{
|
|
$config = null;
|
|
|
|
/*
|
|
* Look for $this->config->view['list']
|
|
*/
|
|
if (
|
|
isset($this->config->{$mode}) &&
|
|
array_key_exists($type, $this->config->{$mode})
|
|
) {
|
|
$config = $this->config->{$mode}[$type];
|
|
}
|
|
/*
|
|
* Look for $this->config->list
|
|
*/
|
|
elseif (isset($this->config->{$type})) {
|
|
$config = $this->config->{$type};
|
|
}
|
|
|
|
/*
|
|
* Apply substitutes:
|
|
*
|
|
* - view.list => manage.list
|
|
*/
|
|
if (!$config) {
|
|
if ($mode == 'manage' && $type == 'list') {
|
|
return $this->makeConfigForMode('view', $type);
|
|
}
|
|
|
|
if ($throwException) {
|
|
throw new ApplicationException('Missing configuration for '.$mode.'.'.$type.' in RelationController definition '.$this->field);
|
|
}
|
|
else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return $this->makeConfig($config);
|
|
}
|
|
|
|
}
|