Repository: Accept already resolved columns as well

If a column was aliased, one was required to use the alias when
selecting, sorting or filtering. This is now not necessary anymore
as it's now possible to use the actual column name as well.
This commit is contained in:
Johannes Meyer 2015-06-26 13:07:21 +02:00
parent 4fc7b3eb1b
commit 4ba84903f1
3 changed files with 308 additions and 78 deletions

View File

@ -69,14 +69,28 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
*
* @var array
*/
protected $statementTableMap;
protected $statementAliasTableMap;
/**
* A flattened array to map statement columns to aliases
*
* @var array
*/
protected $statementColumnMap;
protected $statementAliasColumnMap;
/**
* An array to map table names to statement columns
*
* @var array
*/
protected $statementColumnTableMap;
/**
* A flattened array to map aliases to statement columns
*
* @var array
*/
protected $statementColumnAliasMap;
/**
* List of columns where the COLLATE SQL-instruction has been removed
@ -348,13 +362,13 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
*
* @return array
*/
protected function getStatementTableMap()
protected function getStatementAliasTableMap()
{
if ($this->statementTableMap === null) {
if ($this->statementAliasTableMap === null) {
$this->initializeStatementMaps();
}
return $this->statementTableMap;
return $this->statementAliasTableMap;
}
/**
@ -362,40 +376,87 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
*
* @return array
*/
protected function getStatementColumnMap()
protected function getStatementAliasColumnMap()
{
if ($this->statementColumnMap === null) {
if ($this->statementAliasColumnMap === null) {
$this->initializeStatementMaps();
}
return $this->statementColumnMap;
return $this->statementAliasColumnMap;
}
/**
* Initialize $this->statementTableMap and $this->statementColumnMap
* Return an array to map table names to statement columns
*
* @return array
*/
protected function getStatementColumnTableMap()
{
if ($this->statementColumnTableMap === null) {
$this->initializeStatementMaps();
}
return $this->statementColumnTableMap;
}
/**
* Return a flattened array to map aliases to statement columns
*
* @return array
*/
protected function getStatementColumnAliasMap()
{
if ($this->statementColumnAliasMap === null) {
$this->initializeStatementMaps();
}
return $this->statementColumnAliasMap;
}
/**
* Initialize $this->statementAliasTableMap and $this->statementAliasColumnMap
*/
protected function initializeStatementMaps()
{
$this->statementTableMap = array();
$this->statementColumnMap = array();
$this->statementAliasTableMap = array();
$this->statementAliasColumnMap = array();
$this->statementColumnTableMap = array();
$this->statementColumnAliasMap = array();
foreach ($this->getStatementColumns() as $table => $columns) {
foreach ($columns as $alias => $column) {
$key = is_string($alias) ? $alias : $column;
if (array_key_exists($key, $this->statementTableMap)) {
if ($this->statementTableMap[$key] !== null) {
$existingTable = $this->statementTableMap[$key];
$existingColumn = $this->statementColumnMap[$key];
$this->statementTableMap[$existingTable . '.' . $key] = $existingTable;
$this->statementColumnMap[$existingTable . '.' . $key] = $existingColumn;
$this->statementTableMap[$key] = null;
$this->statementColumnMap[$key] = null;
if (array_key_exists($key, $this->statementAliasTableMap)) {
if ($this->statementAliasTableMap[$key] !== null) {
$existingTable = $this->statementAliasTableMap[$key];
$existingColumn = $this->statementAliasColumnMap[$key];
$this->statementAliasTableMap[$existingTable . '.' . $key] = $existingTable;
$this->statementAliasColumnMap[$existingTable . '.' . $key] = $existingColumn;
$this->statementAliasTableMap[$key] = null;
$this->statementAliasColumnMap[$key] = null;
}
$this->statementTableMap[$table . '.' . $key] = $table;
$this->statementColumnMap[$table . '.' . $key] = $column;
$this->statementAliasTableMap[$table . '.' . $key] = $table;
$this->statementAliasColumnMap[$table . '.' . $key] = $column;
} else {
$this->statementTableMap[$key] = $table;
$this->statementColumnMap[$key] = $column;
$this->statementAliasTableMap[$key] = $table;
$this->statementAliasColumnMap[$key] = $column;
}
if (array_key_exists($column, $this->statementColumnTableMap)) {
if ($this->statementColumnTableMap[$column] !== null) {
$existingTable = $this->statementColumnTableMap[$column];
$existingAlias = $this->statementColumnAliasMap[$column];
$this->statementColumnTableMap[$existingTable . '.' . $column] = $existingTable;
$this->statementColumnAliasMap[$existingTable . '.' . $column] = $existingAlias;
$this->statementColumnTableMap[$column] = null;
$this->statementColumnAliasMap[$column] = null;
}
$this->statementColumnTableMap[$table . '.' . $column] = $table;
$this->statementColumnAliasMap[$table . '.' . $column] = $key;
} else {
$this->statementColumnTableMap[$column] = $table;
$this->statementColumnAliasMap[$column] = $key;
}
}
}
@ -529,6 +590,28 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
return parent::resolveQueryColumnAlias($this->removeTablePrefix($this->clearTableAlias($table)), $alias);
}
/**
* Return the alias for the given query column name or null in case the query column name does not exist
*
* @param array|string $table
* @param string $column
*
* @return string|null
*/
public function reassembleQueryColumnAlias($table, $column)
{
$alias = parent::reassembleQueryColumnAlias($this->removeTablePrefix($this->clearTableAlias($table)), $column);
if (
$alias === null
&& !$this->validateQueryColumnAssociation($table, $column)
&& ($tableName = $this->findTableName($column))
) {
return parent::reassembleQueryColumnAlias($tableName, $column);
}
return $alias;
}
/**
* Return whether the given query column name or alias is available in the given table
*
@ -607,14 +690,35 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
*/
public function resolveStatementColumnAlias($table, $alias)
{
$statementColumnMap = $this->getStatementColumnMap();
if (isset($statementColumnMap[$alias])) {
return $statementColumnMap[$alias];
$statementAliasColumnMap = $this->getStatementAliasColumnMap();
if (isset($statementAliasColumnMap[$alias])) {
return $statementAliasColumnMap[$alias];
}
$prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias;
if (isset($statementColumnMap[$prefixedAlias])) {
return $statementColumnMap[$prefixedAlias];
if (isset($statementAliasColumnMap[$prefixedAlias])) {
return $statementAliasColumnMap[$prefixedAlias];
}
}
/**
* Return the alias for the given statement column name or null in case the statement column does not exist
*
* @param string $table
* @param string $column
*
* @return string|null
*/
public function reassembleStatementColumnAlias($table, $column)
{
$statementColumnAliasMap = $this->getStatementColumnAliasMap();
if (isset($statementColumnAliasMap[$column])) {
return $statementColumnAliasMap[$column];
}
$prefixedColumn = $this->removeTablePrefix($table) . '.' . $column;
if (isset($statementColumnAliasMap[$prefixedColumn])) {
return $statementColumnAliasMap[$prefixedColumn];
}
}
@ -628,13 +732,20 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
*/
public function validateStatementColumnAssociation($table, $alias)
{
$statementTableMap = $this->getStatementTableMap();
if (isset($statementTableMap[$alias])) {
return $statementTableMap[$alias] === $this->removeTablePrefix($table);
$tableName = $this->removeTablePrefix($table);
$statementAliasTableMap = $this->getStatementAliasTableMap();
if (isset($statementAliasTableMap[$alias])) {
return $statementAliasTableMap[$alias] === $tableName;
}
$prefixedAlias = $this->removeTablePrefix($table) . '.' . $alias;
return isset($statementTableMap[$prefixedAlias]);
$statementColumnTableMap = $this->getStatementColumnTableMap();
if (isset($statementColumnTableMap[$alias])) {
return $statementColumnTableMap[$alias] === $tableName;
}
$prefixedAlias = $tableName . '.' . $alias;
return isset($statementAliasTableMap[$prefixedAlias]) || isset($statementColumnTableMap[$prefixedAlias]);
}
/**
@ -648,7 +759,8 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
public function hasStatementColumn($table, $name)
{
if (
$this->resolveStatementColumnAlias($table, $name) === null
($this->resolveStatementColumnAlias($table, $name) === null
&& $this->reassembleStatementColumnAlias($table, $name) === null)
|| !$this->validateStatementColumnAssociation($table, $name)
) {
return parent::hasStatementColumn($table, $name);
@ -669,11 +781,15 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
*/
public function requireStatementColumn($table, $name)
{
if (($column = $this->resolveStatementColumnAlias($table, $name)) === null) {
if (($column = $this->resolveStatementColumnAlias($table, $name)) !== null) {
$alias = $name;
} elseif (($alias = $this->reassembleStatementColumnAlias($table, $name)) !== null) {
$column = $name;
} else {
return parent::requireStatementColumn($table, $name);
}
if (! $this->validateStatementColumnAssociation($table, $name)) {
if (! $this->validateStatementColumnAssociation($table, $alias)) {
throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
}
@ -706,7 +822,9 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
);
}
$column = $this->resolveQueryColumnAlias($tableName, $name);
if (($column = $this->resolveQueryColumnAlias($tableName, $name)) === null) {
$column = $name;
}
$prefixedTableName = $this->prependTablePrefix($tableName);
if ($query->getQuery()->hasJoinedTable($prefixedTableName)) {
@ -741,11 +859,16 @@ abstract class DbRepository extends Repository implements Extensible, Updatable,
return $aliasTableMap[$column];
}
$columnTableMap = $this->getColumnTableMap();
if (isset($columnTableMap[$column])) {
return $columnTableMap[$column];
}
// TODO(jom): Elaborate whether it makes sense to throw ProgrammingError
// instead (duplicate aliases in different tables?)
foreach ($aliasTableMap as $alias => $table) {
if (strpos($alias, '.') !== false) {
list($_, $alias) = explode('.', $column, 2);
foreach ($aliasTableMap as $prefixedAlias => $table) {
if (strpos($prefixedAlias, '.') !== false) {
list($_, $alias) = explode('.', $prefixedAlias, 2);
if ($alias === $column) {
return $table;
}

View File

@ -134,6 +134,20 @@ abstract class Repository implements Selectable
*/
protected $aliasColumnMap;
/**
* An array to map table names to query columns
*
* @var array
*/
protected $columnTableMap;
/**
* A flattened array to map aliases to query columns
*
* @var array
*/
protected $columnAliasMap;
/**
* Create a new repository object
*
@ -144,6 +158,8 @@ abstract class Repository implements Selectable
$this->ds = $ds;
$this->aliasTableMap = array();
$this->aliasColumnMap = array();
$this->columnTableMap = array();
$this->columnAliasMap = array();
$this->init();
}
@ -347,6 +363,34 @@ abstract class Repository implements Selectable
return $this->aliasColumnMap;
}
/**
* Return an array to map table names to query columns
*
* @return array
*/
protected function getColumnTableMap()
{
if (empty($this->columnTableMap)) {
$this->initializeAliasMaps();
}
return $this->columnTableMap;
}
/**
* Return a flattened array to map aliases to query columns
*
* @return array
*/
protected function getColumnAliasMap()
{
if (empty($this->columnAliasMap)) {
$this->initializeAliasMaps();
}
return $this->columnAliasMap;
}
/**
* Initialize $this->aliasTableMap and $this->aliasColumnMap
*
@ -384,6 +428,23 @@ abstract class Repository implements Selectable
$this->aliasTableMap[$key] = $table;
$this->aliasColumnMap[$key] = $column;
}
if (array_key_exists($column, $this->columnTableMap)) {
if ($this->columnTableMap[$column] !== null) {
$existingTable = $this->columnTableMap[$column];
$existingAlias = $this->columnAliasMap[$column];
$this->columnTableMap[$existingTable . '.' . $column] = $existingTable;
$this->columnAliasMap[$existingTable . '.' . $column] = $existingAlias;
$this->columnTableMap[$column] = null;
$this->columnAliasMap[$column] = null;
}
$this->columnTableMap[$table . '.' . $column] = $table;
$this->columnAliasMap[$table . '.' . $column] = $key;
} else {
$this->columnTableMap[$column] = $table;
$this->columnAliasMap[$column] = $key;
}
}
}
}
@ -472,32 +533,35 @@ abstract class Repository implements Selectable
}
$tableRules = $conversionRules[$table];
if (($alias = $this->reassembleQueryColumnAlias($table, $name)) === null) {
$alias = $name;
}
// Check for a conversion method for the alias/column first
if (array_key_exists($name, $tableRules) || in_array($name, $tableRules)) {
$methodName = $context . join('', array_map('ucfirst', explode('_', $name)));
if (array_key_exists($alias, $tableRules) || in_array($alias, $tableRules)) {
$methodName = $context . join('', array_map('ucfirst', explode('_', $alias)));
if (method_exists($this, $methodName)) {
return $methodName;
}
}
// The conversion method for the type is just a fallback, but it is required to exist if defined
if (isset($tableRules[$name])) {
$identifier = join('', array_map('ucfirst', explode('_', $tableRules[$name])));
if (isset($tableRules[$alias])) {
$identifier = join('', array_map('ucfirst', explode('_', $tableRules[$alias])));
if (! method_exists($this, $context . $identifier)) {
// Do not throw an error in case at least one conversion method exists
if (! method_exists($this, ($context === 'persist' ? 'retrieve' : 'persist') . $identifier)) {
throw new ProgrammingError(
'Cannot find any conversion method for type "%s"'
. '. Add a proper conversion method or remove the type definition',
$tableRules[$name]
$tableRules[$alias]
);
}
Logger::debug(
'Conversion method "%s" for type definition "%s" does not exist in repository "%s".',
$context . $identifier,
$tableRules[$name],
$tableRules[$alias],
$this->getName()
);
} else {
@ -729,6 +793,27 @@ abstract class Repository implements Selectable
}
}
/**
* Return the alias for the given query column name or null in case the query column name does not exist
*
* @param string $table
* @param string $column
*
* @return string|null
*/
public function reassembleQueryColumnAlias($table, $column)
{
$columnAliasMap = $this->getColumnAliasMap();
if (isset($columnAliasMap[$column])) {
return $columnAliasMap[$column];
}
$prefixedColumn = $table . '.' . $column;
if (isset($columnAliasMap[$prefixedColumn])) {
return $columnAliasMap[$prefixedColumn];
}
}
/**
* Return whether the given alias or query column name is available in the given table
*
@ -744,8 +829,13 @@ abstract class Repository implements Selectable
return $aliasTableMap[$alias] === $table;
}
$columnTableMap = $this->getColumnTableMap();
if (isset($columnTableMap[$alias])) {
return $columnTableMap[$alias] === $table;
}
$prefixedAlias = $table . '.' . $alias;
return isset($aliasTableMap[$prefixedAlias]);
return isset($aliasTableMap[$prefixedAlias]) || isset($columnTableMap[$prefixedAlias]);
}
/**
@ -758,12 +848,13 @@ abstract class Repository implements Selectable
*/
public function hasQueryColumn($table, $name)
{
if (in_array($name, $this->getFilterColumns())) {
if ($this->resolveQueryColumnAlias($table, $name) !== null) {
$alias = $name;
} elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) === null) {
return false;
}
return $this->resolveQueryColumnAlias($table, $name) !== null
&& $this->validateQueryColumnAssociation($table, $name);
return !in_array($alias, $this->getFilterColumns()) && $this->validateQueryColumnAssociation($table, $name);
}
/**
@ -779,15 +870,19 @@ abstract class Repository implements Selectable
*/
public function requireQueryColumn($table, $name, RepositoryQuery $query = null)
{
if (in_array($name, $this->getFilterColumns())) {
throw new QueryException(t('Filter column "%s" cannot be queried'), $name);
}
if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) {
$alias = $name;
} elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) {
$column = $name;
} else {
throw new QueryException(t('Query column "%s" not found'), $name);
}
if (! $this->validateQueryColumnAssociation($table, $name)) {
if (in_array($alias, $this->getFilterColumns())) {
throw new QueryException(t('Filter column "%s" cannot be queried'), $name);
}
if (! $this->validateQueryColumnAssociation($table, $alias)) {
throw new QueryException(t('Query column "%s" not found in table "%s"'), $name, $table);
}
@ -804,7 +899,8 @@ abstract class Repository implements Selectable
*/
public function hasFilterColumn($table, $name)
{
return $this->resolveQueryColumnAlias($table, $name) !== null
return ($this->resolveQueryColumnAlias($table, $name) !== null
|| $this->reassembleQueryColumnAlias($table, $name) !== null)
&& $this->validateQueryColumnAssociation($table, $name);
}
@ -821,11 +917,15 @@ abstract class Repository implements Selectable
*/
public function requireFilterColumn($table, $name, RepositoryQuery $query = null)
{
if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) {
$alias = $name;
} elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) {
$column = $name;
} else {
throw new QueryException(t('Filter column "%s" not found'), $name);
}
if (! $this->validateQueryColumnAssociation($table, $name)) {
if (! $this->validateQueryColumnAssociation($table, $alias)) {
throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table);
}
@ -857,15 +957,19 @@ abstract class Repository implements Selectable
*/
public function requireStatementColumn($table, $name)
{
if (in_array($name, $this->filterColumns)) {
throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name);
}
if (($column = $this->resolveQueryColumnAlias($table, $name)) === null) {
if (($column = $this->resolveQueryColumnAlias($table, $name)) !== null) {
$alias = $name;
} elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) !== null) {
$column = $name;
} else {
throw new StatementException('Statement column "%s" not found', $name);
}
if (! $this->validateQueryColumnAssociation($table, $name)) {
if (in_array($alias, $this->getFilterColumns())) {
throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name);
}
if (! $this->validateQueryColumnAssociation($table, $alias)) {
throw new StatementException('Statement column "%s" not found in table "%s"', $name, $table);
}

View File

@ -241,20 +241,23 @@ class RepositoryQuery implements QueryInterface, Iterator
if ($direction !== null || !array_key_exists('order', $sortColumns)) {
$sortColumns['order'] = $direction ?: static::SORT_ASC;
}
} elseif (! $ignoreDefault && array_key_exists($field, $sortRules)) {
$sortColumns = $sortRules[$field];
if (! array_key_exists('columns', $sortColumns)) {
$sortColumns['columns'] = array($field);
}
if ($direction !== null || !array_key_exists('order', $sortColumns)) {
$sortColumns['order'] = $direction ?: static::SORT_ASC;
}
} else {
$sortColumns = array(
'columns' => array($field),
'order' => $direction
);
};
$alias = $this->repository->reassembleQueryColumnAlias($this->target, $field) ?: $field;
if (! $ignoreDefault && array_key_exists($alias, $sortRules)) {
$sortColumns = $sortRules[$alias];
if (! array_key_exists('columns', $sortColumns)) {
$sortColumns['columns'] = array($alias);
}
if ($direction !== null || !array_key_exists('order', $sortColumns)) {
$sortColumns['order'] = $direction ?: static::SORT_ASC;
}
} else {
$sortColumns = array(
'columns' => array($alias),
'order' => $direction
);
}
}
$baseDirection = strtoupper($sortColumns['order']) === static::SORT_DESC ? static::SORT_DESC : static::SORT_ASC;