Full pivot support added to RelationController

This commit is contained in:
Samuel Georges 2015-03-19 20:00:31 +11:00
parent 4feb64fef3
commit 398177b006
8 changed files with 281 additions and 167 deletions

View File

@ -1,3 +1,9 @@
* **Build 22x** (2015-03-xx)
- Belongs-to-many model relations now support defining a custom pivot model with the `pivotModel` option (see Database > Model docs).
- The config definitions for behavior `RelationController` have been refactored. When using `pivot` mode all columns and fields should now reside in a `pivot[]` array (see Backend > Relations docs).
- Record Finder form widget now supports nested attributes for relations.
- List columns now support using array names (eg: `relation[attribute]`) which acts as an alias for the `valueFrom` option with `searchable` and `sortable` disabled.
* **Build 226** (2015-03-16)
- Form Tabs now support specifying a default tab using the `defaultTab` option (see Backend > Forms docs).
- Improved the Theme management features: Edit properties, import, export, duplicate and delete.

View File

@ -7,7 +7,6 @@ use Event;
use Input;
use Redirect;
use Backend;
use Backend\Classes\FormField;
use Backend\Classes\ControllerBehavior;
use October\Rain\Router\Helper as RouterHelper;
use ApplicationException;
@ -22,6 +21,8 @@ use Exception;
*/
class FormController extends ControllerBehavior
{
use \Backend\Traits\FormModelSaver;
/**
* @var string Default context for "create" pages.
*/
@ -59,11 +60,6 @@ class FormController extends ControllerBehavior
*/
protected $context;
/**
* @var array List of prepared models that require saving.
*/
protected $modelsToSave = [];
/**
* @var Model The initialized model used by the form.
*/
@ -701,43 +697,4 @@ class FormController extends ControllerBehavior
});
}
//
// Internals
//
protected function prepareModelsToSave($model, $saveData)
{
$this->modelsToSave = [];
$this->setModelAttributes($model, $saveData);
return $this->modelsToSave;
}
/**
* Sets a data collection to a model attributes, relations will also be set.
* @param array $saveData Data to save.
* @param Model $model Model to save to
* @return array The collection of models to save.
*/
protected function setModelAttributes($model, $saveData)
{
$this->modelsToSave[] = $model;
if (!is_array($saveData)) {
return;
}
$singularTypes = ['belongsTo', 'hasOne', 'morphOne'];
foreach ($saveData as $attribute => $value) {
if (
is_array($value) &&
$model->hasRelation($attribute) &&
in_array($model->getRelationType($attribute), $singularTypes)
) {
$this->setModelAttributes($model->{$attribute}, $value);
}
elseif ($value !== FormField::NO_SAVE_DATA) {
$model->{$attribute} = $value;
}
}
}
}

View File

@ -17,6 +17,8 @@ use October\Rain\Database\Model;
*/
class RelationController extends ControllerBehavior
{
use \Backend\Traits\FormModelSaver;
/**
* @var const Postback parameter for the active relationship field.
*/
@ -272,89 +274,6 @@ class RelationController extends ControllerBehavior
}
}
/**
* 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';
}
}
/**
* Renders the relationship manager.
* @param string $field The relationship field.
@ -889,8 +808,14 @@ class RelationController extends ControllerBehavior
{
$this->beforeAjax();
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
$hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first();
$saveData = $this->pivotWidget->getSaveData();
$this->relationObject->updateExistingPivot($this->manageId, $saveData, true);
$modelsToSave = $this->prepareModelsToSave($hydratedModel, $saveData);
foreach ($modelsToSave as $modelToSave) {
$modelToSave->save();
}
return ['#'.$this->relationGetId('view') => $this->relationRenderView()];
}
@ -960,7 +885,7 @@ class RelationController extends ControllerBehavior
* Multiple (has many, belongs to many)
*/
if ($this->viewMode == 'multi') {
$config = $this->makeConfig($this->config->list);
$config = $this->makeConfigForMode('view', 'list');
$config->model = $this->relationModel;
$config->alias = $this->alias . 'ViewList';
$config->showSorting = $this->getConfig('view[showSorting]', true);
@ -1027,7 +952,7 @@ class RelationController extends ControllerBehavior
$this->controller->relationExtendQuery($query, $this->field);
$this->viewModel = $query->getResults() ?: $this->relationModel;
$config = $this->makeConfig($this->config->form);
$config = $this->makeConfigForMode('view', 'form');
$config->model = $this->viewModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = 'relation';
@ -1046,13 +971,14 @@ class RelationController extends ControllerBehavior
/*
* Pivot
*/
if ($this->manageMode == 'pivot' && isset($this->config->list)) {
$config = $this->makeConfig($this->config->list);
if ($this->manageMode == 'pivot' && isset($this->config->pivot)) {
$config = $this->makeConfigForMode('manage', 'list');
$config->model = $this->relationModel;
$config->alias = $this->alias . 'ManagePivotList';
$config->showSetup = false;
$config->defaultSort = $this->getConfig('pivot[defaultSort]');
$config->recordsPerPage = $this->getConfig('pivot[recordsPerPage]');
$config->showSorting = $this->getConfig('manage[showSorting]', false);
$config->defaultSort = $this->getConfig('manage[defaultSort]');
$config->recordsPerPage = $this->getConfig('manage[recordsPerPage]');
$config->recordOnClick = sprintf(
"$.oc.relationBehavior.clickManagePivotListRecord(:id, '%s', '%s')",
$this->field,
@ -1079,7 +1005,7 @@ class RelationController extends ControllerBehavior
* List
*/
elseif ($this->manageMode == 'list' && isset($this->config->list)) {
$config = $this->makeConfig($this->config->list);
$config = $this->makeConfigForMode('manage', 'list');
$config->model = $this->relationModel;
$config->alias = $this->alias . 'ManageList';
$config->showSetup = false;
@ -1131,7 +1057,7 @@ class RelationController extends ControllerBehavior
}
}
$config = $this->makeConfig($this->config->form);
$config = $this->makeConfigForMode('manage', 'form');
$config->model = $this->relationModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = $context ?: 'relation';
@ -1179,7 +1105,7 @@ class RelationController extends ControllerBehavior
protected function makePivotWidget()
{
$config = $this->makeConfig($this->config->pivot);
$config = $this->makeConfigForMode('pivot', 'form');
$config->model = $this->relationModel;
$config->arrayName = class_basename($this->relationModel);
$config->context = 'relation';
@ -1189,10 +1115,11 @@ class RelationController extends ControllerBehavior
* Existing record
*/
if ($this->manageId) {
$config->model = $this->relationModel->find($this->manageId);
$config->data = $this->relationObject->newPivotStatementForId($this->manageId)->first();
$foreignKeyName = $this->relationModel->getQualifiedKeyName();
$hydratedModel = $this->relationObject->where($foreignKeyName, $this->manageId)->first();
if (!$config->model || !$config->data) {
$config->model = $hydratedModel;
if (!$config->model) {
throw new ApplicationException(Lang::get('backend::lang.model.not_found', [
'class' => get_class($config->model), 'id' => $this->manageId
]));
@ -1222,4 +1149,134 @@ class RelationController extends ControllerBehavior
return $this->sessionKey = FormHelper::getSessionKey();
}
//
// Helpers
//
/**
* 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';
}
}
/**
* 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')
{
$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);
}
throw new ApplicationException('Missing configuration for '.$mode.'.'.$type.' in RelationController definition '.$this->field);
}
return $this->makeConfig($config);
}
}

View File

@ -5,6 +5,7 @@
<!-- Passable fields -->
<input type="hidden" name="manage_id" value="<?= $relationManageId ?>" />
<input type="hidden" name="_relation_field" value="<?= $relationField ?>" />
<input type="hidden" name="_relation_mode" value="form" />
<input type="hidden" name="_relation_session_key" value="<?= $relationSessionKey ?>" />
<div class="modal-header">

View File

@ -4,7 +4,7 @@ use Str;
use Input;
use Validator;
use System\Models\File;
use SystemException;
use ApplicationException;
use Backend\Classes\FormField;
use Backend\Classes\FormWidgetBase;
use ValidationException;
@ -182,6 +182,14 @@ class FileUpload extends FormWidgetBase
protected function getRelationObject()
{
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
if (!$model->hasRelation($attribute)) {
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', [
'class' => get_class($model),
'relation' => $attribute
]));
}
return $model->{$attribute}();
}
@ -230,7 +238,7 @@ class FileUpload extends FormWidgetBase
return $this->makePartial('config_form');
}
throw new SystemException('Unable to find file, it may no longer exist');
throw new ApplicationException('Unable to find file, it may no longer exist');
}
/**
@ -248,7 +256,7 @@ class FileUpload extends FormWidgetBase
return ['item' => $file->toArray()];
}
throw new SystemException('Unable to find file, it may no longer exist');
throw new ApplicationException('Unable to find file, it may no longer exist');
}
catch (Exception $ex) {
return json_encode(['error' => $ex->getMessage()]);
@ -300,7 +308,7 @@ class FileUpload extends FormWidgetBase
}
if (!$uploadedFile->isValid()) {
throw new SystemException('File is not valid');
throw new ApplicationException('File is not valid');
}
$fileRelation = $this->getRelationObject();

View File

@ -1,8 +1,8 @@
<?php namespace Backend\FormWidgets;
use Lang;
use ApplicationException;
use Backend\Classes\FormWidgetBase;
use SystemException;
/**
* Record Finder
@ -54,16 +54,6 @@ class RecordFinder extends FormWidgetBase
*/
protected $defaultAlias = 'recordfinder';
/**
* @var string Relationship type
*/
public $relationType;
/**
* @var string Relationship name
*/
public $relationName;
/**
* @var Model Relationship model
*/
@ -91,16 +81,6 @@ class RecordFinder extends FormWidgetBase
'descriptionFrom',
]);
$this->relationName = $this->valueFrom;
$this->relationType = $this->model->getRelationType($this->relationName);
if (!$this->model->hasRelation($this->relationName)) {
throw new SystemException(Lang::get('backend::lang.model.missing_relation', [
'class' => get_class($this->model),
'relation' => $this->relationName
]));
}
if (post('recordfinder_flag')) {
$this->listWidget = $this->makeListWidget();
$this->listWidget->bindToController();
@ -120,6 +100,36 @@ class RecordFinder extends FormWidgetBase
}
}
/**
* Returns the value as a relation object from the model,
* supports nesting via HTML array.
* @return Relation
*/
protected function getRelationObject()
{
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
if (!$model->hasRelation($attribute)) {
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', [
'class' => get_class($model),
'relation' => $attribute
]));
}
return $model->{$attribute}();
}
/**
* Returns the model of a relation type,
* supports nesting via HTML array.
* @return Relation
*/
protected function getRelationModel()
{
list($model, $attribute) = $this->resolveModelAttribute($this->valueFrom);
return $model->makeRelation($attribute);
}
/**
* {@inheritDoc}
*/
@ -220,7 +230,7 @@ class RecordFinder extends FormWidgetBase
protected function makeListWidget()
{
$config = $this->makeConfig($this->getConfig('list'));
$config->model = $this->model->makeRelation($this->relationName);
$config->model = $this->getRelationModel();
$config->alias = $this->alias . 'List';
$config->showSetup = false;
$config->showCheckboxes = false;

View File

@ -0,0 +1,69 @@
<?php namespace Backend\Traits;
use Backend\Classes\FormField;
/**
* Form Model Saver Trait
*
* Special logic for applying form data (usually from postback) and
* applying it to a model and its relationships. This is a customized,
* safer and simplified version of $model->push().
*
* Usage:
*
* $modelsToSave = $this->prepareModelsToSave($model, [...]);
*
* foreach ($modelsToSave as $modelToSave) {
* $modelToSave->save();
* }
*
* @package october\backend
* @author Alexey Bobkov, Samuel Georges
*/
trait FormModelSaver
{
/**
* @var array List of prepared models that require saving.
*/
protected $modelsToSave = [];
protected function prepareModelsToSave($model, $saveData)
{
$this->modelsToSave = [];
$this->setModelAttributes($model, $saveData);
return $this->modelsToSave;
}
/**
* Sets a data collection to a model attributes, relations will also be set.
* @param array $saveData Data to save.
* @param Model $model Model to save to
* @return array The collection of models to save.
*/
protected function setModelAttributes($model, $saveData)
{
$this->modelsToSave[] = $model;
if (!is_array($saveData)) {
return;
}
$singularTypes = ['belongsTo', 'hasOne', 'morphOne'];
foreach ($saveData as $attribute => $value) {
$isNested = $attribute == 'pivot' || (
$model->hasRelation($attribute) &&
in_array($model->getRelationType($attribute), $singularTypes)
);
if ($isNested && is_array($value)) {
$this->setModelAttributes($model->{$attribute}, $value);
}
elseif ($value !== FormField::NO_SAVE_DATA) {
$model->{$attribute} = $value;
}
}
}
}

View File

@ -660,6 +660,12 @@ class Lists extends WidgetBase
$label = studly_case($name);
}
if (strpos($name, '[') !== false && strpos($name, ']') !== false) {
$config['valueFrom'] = $name;
$config['sortable'] = false;
$config['searchable'] = false;
}
$columnType = isset($config['type']) ? $config['type'] : null;
$column = new ListColumn($name, $label);