Fixes #530 - No more joinWith() and groupBy(), align with Laravel to the best of our ability.

Adds new list column type "nameFrom" (take name from X attribute) as an alternative to "select".
This commit is contained in:
Sam Georges 2014-08-16 14:08:51 +10:00
parent 982b22a676
commit d52fe388db
2 changed files with 170 additions and 51 deletions

View File

@ -41,7 +41,14 @@ class ListColumn
public $sortable = true;
/**
* @var string Custom SQL for selecting this record value.
* @var string Model attribute to use for the display value, this will
* override any $sqlSelect definition.
*/
public $nameFrom;
/**
* @var string Custom SQL for selecting this record display value,
* the @ symbol is replaced with the table name.
*/
public $sqlSelect;
@ -103,6 +110,7 @@ class ListColumn
if (isset($config['searchable'])) $this->searchable = $config['searchable'];
if (isset($config['sortable'])) $this->sortable = $config['sortable'];
if (isset($config['invisible'])) $this->invisible = $config['invisible'];
if (isset($config['nameFrom'])) $this->nameFrom = $config['nameFrom'];
if (isset($config['select'])) $this->sqlSelect = $config['select'];
if (isset($config['relation'])) $this->relation = $config['relation'];
if (isset($config['format'])) $this->format = $config['format'];

View File

@ -265,9 +265,10 @@ class Lists extends WidgetBase
protected function prepareModel()
{
$query = $this->model->newQuery();
$selects = [$this->model->getTable().'.*'];
$tables = ['base'=>$this->model->getTable()];
$primaryTable = $this->model->getTable();
$selects = [$primaryTable.'.*'];
$joins = [];
$withs = [];
/*
* Extensibility
@ -276,68 +277,129 @@ class Lists extends WidgetBase
$this->fireEvent('list.extendQueryBefore', [$query]);
/*
* Related custom selects, must come first
* Prepare searchable column names
*/
foreach ($this->getVisibleListColumns() as $column) {
if (!isset($column->relation) || !isset($column->sqlSelect))
continue;
$primarySearchable = [];
$relationSearchable = [];
if (!$this->model->hasRelation($column->relation))
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', ['class'=>get_class($this->model), 'relation'=>$column->relation]));
$columnsToSearch = [];
if (!empty($this->searchTerm) && ($searchableColumns = $this->getSearchableColumns())) {
foreach ($searchableColumns as $column) {
/*
* Related
*/
if ($this->isColumnRelated($column)) {
$table = $this->model->makeRelation($column->relation)->getTable();
$columnName = isset($column->sqlSelect)
? DbDongle::raw($this->parseTableName($column->sqlSelect, $table))
: $table . '.' . $column->nameFrom;
$alias = Db::getQueryGrammar()->wrap($column->columnName);
$table = $this->model->makeRelation($column->relation)->getTable();
$relationType = $this->model->getRelationType($column->relation);
$sqlSelect = $this->parseTableName($column->sqlSelect, $table);
$relationSearchable[$column->relation][] = $columnName;
}
/*
* Primary
*/
else {
$columnName = isset($column->sqlSelect)
? DbDongle::raw($this->parseTableName($column->sqlSelect, $primaryTable))
: $primaryTable . '.' . $column->columnName;
if (in_array($relationType, ['hasMany', 'belongsToMany', 'morphToMany', 'morphedByMany', 'morphMany', 'attachMany', 'hasManyThrough']))
$selects[] = DbDongle::raw("group_concat(" . $sqlSelect . " separator ', ') as ". $alias);
else
$selects[] = DbDongle::raw($sqlSelect . ' as '. $alias);
$joins[] = $column->relation;
$tables[$column->relation] = $table;
$primarySearchable[] = $columnName;
}
}
}
if ($joins)
$query->joinWith(array_unique($joins), false);
/*
* Prepare related eager loads (withs) and custom selects (joins)
*/
foreach ($this->getVisibleListColumns() as $column) {
if (!$this->isColumnRelated($column) || (!isset($column->sqlSelect) && !isset($column->nameFrom)))
continue;
if (isset($column->nameFrom))
$withs[] = $column->relation;
$joins[] = $column->relation;
}
/*
* Include any relation constraints
*/
if ($joins) {
foreach (array_unique($joins) as $join) {
/*
* Apply a supplied search term for relation columns and
* constrain the query only if there is something to search for
*/
$columnsToSearch = array_get($relationSearchable, $join, []);
if (count($columnsToSearch) > 0) {
$query->whereHas($join, function($_query) use ($columnsToSearch) {
$_query->searchWhere($this->searchTerm, $columnsToSearch);
});
}
}
}
/*
* Add eager loads to the query
*/
if ($withs) {
$query->with(array_unique($withs));
}
/*
* Custom select queries
*/
foreach ($this->getVisibleListColumns() as $column) {
if (!isset($column->sqlSelect) || isset($column->relation))
if (!isset($column->sqlSelect))
continue;
$alias = Db::getQueryGrammar()->wrap($column->columnName);
$sqlSelect = $this->parseTableName($column->sqlSelect, $tables['base']);
$selects[] = DbDongle::raw($sqlSelect . ' as '. $alias);
/*
* Relation column
*/
if (isset($column->relation)) {
$table = $this->model->makeRelation($column->relation)->getTable();
$relationType = $this->model->getRelationType($column->relation);
$sqlSelect = $this->parseTableName($column->sqlSelect, $table);
/*
* Manipulate a count query for the sub query
*/
$relationObj = $this->model->{$column->relation}();
$countQuery = $relationObj->getRelationCountQuery($relationObj->getRelated()->newQuery(), $query);
$joinSql = $this->isColumnRelated($column, true)
? DbDongle::raw("group_concat(" . $sqlSelect . " separator ', ')")
: DbDongle::raw($sqlSelect);
$joinSql = $countQuery->select($joinSql)->toSql();
$selects[] = Db::raw("(".$joinSql.") as ".$alias);
}
/*
* Primary column
*/
else {
$sqlSelect = $this->parseTableName($column->sqlSelect, $primaryTable);
$selects[] = DbDongle::raw($sqlSelect . ' as '. $alias);
}
}
/*
* Handle a supplied search term
* Apply a supplied search term for primary columns
*/
if (!empty($this->searchTerm) && ($searchableColumns = $this->getSearchableColumns())) {
$query->orWhere(function($innerQuery) use ($searchableColumns, $tables) {
$columnsToSearch = [];
foreach ($searchableColumns as $column) {
if (isset($column->sqlSelect)) {
$table = (isset($column->relation)) ? $tables[$column->relation] : 'base';
$columnName = DbDongle::raw($this->parseTableName($column->sqlSelect, $table));
}
else
$columnName = $tables['base'] . '.' . $column->columnName;
$columnsToSearch[] = $columnName;
}
$innerQuery->searchWhere($this->searchTerm, $columnsToSearch);
if (count($primarySearchable) > 0) {
$query->orWhere(function($innerQuery) use ($primarySearchable) {
$innerQuery->searchWhere($this->searchTerm, $primarySearchable);
});
}
/*
* Handle sorting
* Apply sorting
*/
if ($sortColumn = $this->getSortColumn()) {
$query->orderBy($sortColumn, $this->sortDirection);
@ -356,9 +418,7 @@ class Lists extends WidgetBase
Event::fire('backend.list.extendQuery', [$this, $query]);
$this->fireEvent('list.extendQuery', [$query]);
// Grouping due to the joinWith() call
$query->select($selects);
$query->groupBy($this->model->getQualifiedKeyName());
return $query;
}
@ -555,16 +615,33 @@ class Lists extends WidgetBase
*/
public function getColumnValue($record, $column)
{
$columnName = $column->columnName;
/*
* If the column is a relation, it will be a custom select,
* Handle taking name from model attribute.
*/
if ($column->nameFrom) {
if (!array_key_exists($columnName, $record->getRelations()))
$value = null;
elseif ($this->isColumnRelated($column, true))
$value = implode(', ', $record->{$columnName}->lists($column->nameFrom));
elseif ($this->isColumnRelated($column))
$value = $record->{$columnName}->{$column->nameFrom};
else
$value = $record->{$column->nameFrom};
}
/*
* Otherwise, if the column is a relation, it will be a custom select,
* so prevent the Model from attempting to load the relation
* if the value is NULL.
*/
$columnName = $column->columnName;
if ($record->hasRelation($columnName) && array_key_exists($columnName, $record->attributes))
$value = $record->attributes[$columnName];
else
$value = $record->{$columnName};
else {
if ($record->hasRelation($columnName) && array_key_exists($columnName, $record->attributes))
$value = $record->attributes[$columnName];
else
$value = $record->{$columnName};
}
if (method_exists($this, 'eval'. studly_case($column->type) .'TypeValue'))
$value = $this->{'eval'. studly_case($column->type) .'TypeValue'}($record, $column, $value);
@ -962,4 +1039,38 @@ class Lists extends WidgetBase
return $this->onRefresh();
}
//
// Helpers
//
/**
* Check if column refers to a relation of the model
* @param ListColumn $column List column object
* @param boolean $multi If set, returns true only if the relation is a "multiple relation type"
* @return boolean
*/
protected function isColumnRelated($column, $multi = false)
{
if (!isset($column->relation))
return false;
if (!$this->model->hasRelation($column->relation))
throw new ApplicationException(Lang::get('backend::lang.model.missing_relation', ['class'=>get_class($this->model), 'relation'=>$column->relation]));
if (!$multi)
return true;
$relationType = $this->model->getRelationType($column->relation);
return in_array($relationType, [
'hasMany',
'belongsToMany',
'morphToMany',
'morphedByMany',
'morphMany',
'attachMany',
'hasManyThrough'
]);
}
}