diff --git a/modules/backend/classes/ListColumn.php b/modules/backend/classes/ListColumn.php index ea9d14346..7cc7ff096 100644 --- a/modules/backend/classes/ListColumn.php +++ b/modules/backend/classes/ListColumn.php @@ -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']; diff --git a/modules/backend/widgets/Lists.php b/modules/backend/widgets/Lists.php index a0cce838b..8c8216b73 100644 --- a/modules/backend/widgets/Lists.php +++ b/modules/backend/widgets/Lists.php @@ -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' + ]); + } + } \ No newline at end of file