1138 lines
36 KiB
PHP
1138 lines
36 KiB
PHP
<?php
|
|
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
|
|
|
|
namespace Icinga\Repository;
|
|
|
|
use DateTime;
|
|
use Icinga\Application\Logger;
|
|
use Icinga\Data\Filter\Filter;
|
|
use Icinga\Data\Filter\FilterExpression;
|
|
use Icinga\Data\Selectable;
|
|
use Icinga\Exception\ProgrammingError;
|
|
use Icinga\Exception\QueryException;
|
|
use Icinga\Exception\StatementException;
|
|
use Icinga\Util\String;
|
|
|
|
/**
|
|
* Abstract base class for concrete repository implementations
|
|
*
|
|
* To utilize this class and its features, the following is required:
|
|
* <ul>
|
|
* <li>Concrete implementations need to initialize Repository::$queryColumns</li>
|
|
* <li>The datasource passed to a repository must implement the Selectable interface</li>
|
|
* <li>The datasource must yield an instance of Queryable when its select() method is called</li>
|
|
* </ul>
|
|
*/
|
|
abstract class Repository implements Selectable
|
|
{
|
|
/**
|
|
* The format to use when converting values of type date_time
|
|
*/
|
|
const DATETIME_FORMAT = 'd/m/Y g:i A';
|
|
|
|
/**
|
|
* The name of this repository
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $name;
|
|
|
|
/**
|
|
* The datasource being used
|
|
*
|
|
* @var Selectable
|
|
*/
|
|
protected $ds;
|
|
|
|
/**
|
|
* The base table name this repository is responsible for
|
|
*
|
|
* This will be automatically set to the first key of $queryColumns if not explicitly set.
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $baseTable;
|
|
|
|
/**
|
|
* The virtual tables being provided
|
|
*
|
|
* This may be initialized by concrete repository implementations with an array
|
|
* where a key is the name of a virtual table and its value the real table name.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $virtualTables;
|
|
|
|
/**
|
|
* The query columns being provided
|
|
*
|
|
* This must be initialized by concrete repository implementations, in the following format
|
|
* <pre><code>
|
|
* array(
|
|
* 'baseTable' => array(
|
|
* 'column1',
|
|
* 'alias1' => 'column2',
|
|
* 'alias2' => 'column3'
|
|
* )
|
|
* )
|
|
* </code></pre>
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $queryColumns;
|
|
|
|
/**
|
|
* The columns (or aliases) which are not permitted to be queried
|
|
*
|
|
* Blacklisted query columns can still occur in a filter expression or sort rule.
|
|
*
|
|
* @var array An array of strings
|
|
*/
|
|
protected $blacklistedQueryColumns;
|
|
|
|
/**
|
|
* The filter columns being provided
|
|
*
|
|
* This may be intialized by concrete repository implementations, in the following format
|
|
* <pre><code>
|
|
* array(
|
|
* 'alias_or_column_name',
|
|
* 'label_to_show_in_the_filter_editor' => 'alias_or_column_name'
|
|
* )
|
|
* </code></pre>
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $filterColumns;
|
|
|
|
/**
|
|
* The search columns (or aliases) being provided
|
|
*
|
|
* @var array An array of strings
|
|
*/
|
|
protected $searchColumns;
|
|
|
|
/**
|
|
* The sort rules to be applied on a query
|
|
*
|
|
* This may be initialized by concrete repository implementations, in the following format
|
|
* <pre><code>
|
|
* array(
|
|
* 'alias_or_column_name' => array(
|
|
* 'order' => 'asc'
|
|
* ),
|
|
* 'alias_or_column_name' => array(
|
|
* 'columns' => array(
|
|
* 'once_more_the_alias_or_column_name_as_in_the_parent_key',
|
|
* 'an_additional_alias_or_column_name_with_a_specific_direction asc'
|
|
* ),
|
|
* 'order' => 'desc'
|
|
* ),
|
|
* 'alias_or_column_name' => array(
|
|
* 'columns' => array('a_different_alias_or_column_name_designated_to_act_as_the_only_sort_column')
|
|
* // Ascendant sort by default
|
|
* )
|
|
* )
|
|
* </code></pre>
|
|
* Note that it's mandatory to supply the alias name in case there is one.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $sortRules;
|
|
|
|
/**
|
|
* The value conversion rules to apply on a query or statement
|
|
*
|
|
* This may be initialized by concrete repository implementations and describes for which aliases or column
|
|
* names what type of conversion is available. For entries, where the key is the alias/column and the value
|
|
* is the type identifier, the repository attempts to find a conversion method for the alias/column first and,
|
|
* if none is found, then for the type. If an entry only provides a value, which is the alias/column, the
|
|
* repository only attempts to find a conversion method for the alias/column. The name of a conversion method
|
|
* is expected to be declared using lowerCamelCase. (e.g. user_name will be translated to persistUserName and
|
|
* groupname will be translated to retrieveGroupname)
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $conversionRules;
|
|
|
|
/**
|
|
* An array to map table names to aliases
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $aliasTableMap;
|
|
|
|
/**
|
|
* A flattened array to map query columns to aliases
|
|
*
|
|
* @var array
|
|
*/
|
|
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
|
|
*
|
|
* @param Selectable $ds The datasource to use
|
|
*/
|
|
public function __construct(Selectable $ds)
|
|
{
|
|
$this->ds = $ds;
|
|
$this->aliasTableMap = array();
|
|
$this->aliasColumnMap = array();
|
|
$this->columnTableMap = array();
|
|
$this->columnAliasMap = array();
|
|
|
|
$this->init();
|
|
}
|
|
|
|
/**
|
|
* Initialize this repository
|
|
*
|
|
* Supposed to be overwritten by concrete repository implementations.
|
|
*/
|
|
protected function init()
|
|
{
|
|
|
|
}
|
|
|
|
/**
|
|
* Set this repository's name
|
|
*
|
|
* @param string $name
|
|
*
|
|
* @return $this
|
|
*/
|
|
public function setName($name)
|
|
{
|
|
$this->name = $name;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Return this repository's name
|
|
*
|
|
* In case no name has been explicitly set yet, the class name is returned.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getName()
|
|
{
|
|
return $this->name ?: __CLASS__;
|
|
}
|
|
|
|
/**
|
|
* Return the datasource being used
|
|
*
|
|
* @return Selectable
|
|
*/
|
|
public function getDataSource()
|
|
{
|
|
return $this->ds;
|
|
}
|
|
|
|
/**
|
|
* Return the base table name this repository is responsible for
|
|
*
|
|
* @return string
|
|
*
|
|
* @throws ProgrammingError In case no base table name has been set and
|
|
* $this->queryColumns does not provide one either
|
|
*/
|
|
public function getBaseTable()
|
|
{
|
|
if ($this->baseTable === null) {
|
|
$queryColumns = $this->getQueryColumns();
|
|
reset($queryColumns);
|
|
$this->baseTable = key($queryColumns);
|
|
if (is_int($this->baseTable) || !is_array($queryColumns[$this->baseTable])) {
|
|
throw new ProgrammingError('"%s" is not a valid base table', $this->baseTable);
|
|
}
|
|
}
|
|
|
|
return $this->baseTable;
|
|
}
|
|
|
|
/**
|
|
* Return the virtual tables being provided
|
|
*
|
|
* Calls $this->initializeVirtualTables() in case $this->virtualTables is null.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getVirtualTables()
|
|
{
|
|
if ($this->virtualTables === null) {
|
|
$this->virtualTables = $this->initializeVirtualTables();
|
|
}
|
|
|
|
return $this->virtualTables;
|
|
}
|
|
|
|
/**
|
|
* Overwrite this in your repository implementation in case you need to initialize the virtual tables lazily
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function initializeVirtualTables()
|
|
{
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Return the query columns being provided
|
|
*
|
|
* Calls $this->initializeQueryColumns() in case $this->queryColumns is null.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getQueryColumns()
|
|
{
|
|
if ($this->queryColumns === null) {
|
|
$this->queryColumns = $this->initializeQueryColumns();
|
|
}
|
|
|
|
return $this->queryColumns;
|
|
}
|
|
|
|
/**
|
|
* Overwrite this in your repository implementation in case you need to initialize the query columns lazily
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function initializeQueryColumns()
|
|
{
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Return the columns (or aliases) which are not permitted to be queried
|
|
*
|
|
* Calls $this->initializeBlacklistedQueryColumns() in case $this->blacklistedQueryColumns is null.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getBlacklistedQueryColumns()
|
|
{
|
|
if ($this->blacklistedQueryColumns === null) {
|
|
$this->blacklistedQueryColumns = $this->initializeBlacklistedQueryColumns();
|
|
}
|
|
|
|
return $this->blacklistedQueryColumns;
|
|
}
|
|
|
|
/**
|
|
* Overwrite this in your repository implementation in case you
|
|
* need to initialize the blacklisted query columns lazily
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function initializeBlacklistedQueryColumns()
|
|
{
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Return the filter columns being provided
|
|
*
|
|
* Calls $this->initializeFilterColumns() in case $this->filterColumns is null.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getFilterColumns()
|
|
{
|
|
if ($this->filterColumns === null) {
|
|
$this->filterColumns = $this->initializeFilterColumns();
|
|
}
|
|
|
|
return $this->filterColumns;
|
|
}
|
|
|
|
/**
|
|
* Overwrite this in your repository implementation in case you need to initialize the filter columns lazily
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function initializeFilterColumns()
|
|
{
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Return the search columns being provided
|
|
*
|
|
* Calls $this->initializeSearchColumns() in case $this->searchColumns is null.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getSearchColumns()
|
|
{
|
|
if ($this->searchColumns === null) {
|
|
$this->searchColumns = $this->initializeSearchColumns();
|
|
}
|
|
|
|
return $this->searchColumns;
|
|
}
|
|
|
|
/**
|
|
* Overwrite this in your repository implementation in case you need to initialize the search columns lazily
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function initializeSearchColumns()
|
|
{
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Return the sort rules to be applied on a query
|
|
*
|
|
* Calls $this->initializeSortRules() in case $this->sortRules is null.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getSortRules()
|
|
{
|
|
if ($this->sortRules === null) {
|
|
$this->sortRules = $this->initializeSortRules();
|
|
}
|
|
|
|
return $this->sortRules;
|
|
}
|
|
|
|
/**
|
|
* Overwrite this in your repository implementation in case you need to initialize the sort rules lazily
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function initializeSortRules()
|
|
{
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Return the value conversion rules to apply on a query
|
|
*
|
|
* Calls $this->initializeConversionRules() in case $this->conversionRules is null.
|
|
*
|
|
* @return array
|
|
*/
|
|
public function getConversionRules()
|
|
{
|
|
if ($this->conversionRules === null) {
|
|
$this->conversionRules = $this->initializeConversionRules();
|
|
}
|
|
|
|
return $this->conversionRules;
|
|
}
|
|
|
|
/**
|
|
* Overwrite this in your repository implementation in case you need to initialize the conversion rules lazily
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function initializeConversionRules()
|
|
{
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Return an array to map table names to aliases
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getAliasTableMap()
|
|
{
|
|
if (empty($this->aliasTableMap)) {
|
|
$this->initializeAliasMaps();
|
|
}
|
|
|
|
return $this->aliasTableMap;
|
|
}
|
|
|
|
/**
|
|
* Return a flattened array to map query columns to aliases
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function getAliasColumnMap()
|
|
{
|
|
if (empty($this->aliasColumnMap)) {
|
|
$this->initializeAliasMaps();
|
|
}
|
|
|
|
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
|
|
*
|
|
* @throws ProgrammingError In case $this->queryColumns does not provide any column information
|
|
*/
|
|
protected function initializeAliasMaps()
|
|
{
|
|
$queryColumns = $this->getQueryColumns();
|
|
if (empty($queryColumns)) {
|
|
throw new ProgrammingError('Repositories are required to initialize $this->queryColumns first');
|
|
}
|
|
|
|
foreach ($queryColumns as $table => $columns) {
|
|
foreach ($columns as $alias => $column) {
|
|
if (! is_string($alias)) {
|
|
$key = $column;
|
|
} else {
|
|
$key = $alias;
|
|
$column = preg_replace('~\n\s*~', ' ', $column);
|
|
}
|
|
|
|
if (array_key_exists($key, $this->aliasTableMap)) {
|
|
if ($this->aliasTableMap[$key] !== null) {
|
|
$existingTable = $this->aliasTableMap[$key];
|
|
$existingColumn = $this->aliasColumnMap[$key];
|
|
$this->aliasTableMap[$existingTable . '.' . $key] = $existingTable;
|
|
$this->aliasColumnMap[$existingTable . '.' . $key] = $existingColumn;
|
|
$this->aliasTableMap[$key] = null;
|
|
$this->aliasColumnMap[$key] = null;
|
|
}
|
|
|
|
$this->aliasTableMap[$table . '.' . $key] = $table;
|
|
$this->aliasColumnMap[$table . '.' . $key] = $column;
|
|
} else {
|
|
$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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return a new query for the given columns
|
|
*
|
|
* @param array $columns The desired columns, if null all columns will be queried
|
|
*
|
|
* @return RepositoryQuery
|
|
*/
|
|
public function select(array $columns = null)
|
|
{
|
|
$query = new RepositoryQuery($this);
|
|
$query->from($this->getBaseTable(), $columns);
|
|
return $query;
|
|
}
|
|
|
|
/**
|
|
* Return whether this repository is capable of converting values for the given table and optional column
|
|
*
|
|
* @param string $table
|
|
* @param string $column
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function providesValueConversion($table, $column = null)
|
|
{
|
|
$conversionRules = $this->getConversionRules();
|
|
if (empty($conversionRules)) {
|
|
return false;
|
|
}
|
|
|
|
if (! isset($conversionRules[$table])) {
|
|
return false;
|
|
} elseif ($column === null) {
|
|
return true;
|
|
}
|
|
|
|
$alias = $this->reassembleQueryColumnAlias($table, $column) ?: $column;
|
|
return array_key_exists($alias, $conversionRules[$table]) || in_array($alias, $conversionRules[$table]);
|
|
}
|
|
|
|
/**
|
|
* Convert a value supposed to be transmitted to the data source
|
|
*
|
|
* @param string $table The table where to persist the value
|
|
* @param string $name The alias or column name
|
|
* @param mixed $value The value to convert
|
|
* @param RepositoryQuery $query An optional query to pass as context
|
|
* (Directly passed through to $this->getConverter)
|
|
*
|
|
* @return mixed If conversion was possible, the converted value,
|
|
* otherwise the unchanged value
|
|
*/
|
|
public function persistColumn($table, $name, $value, RepositoryQuery $query = null)
|
|
{
|
|
$converter = $this->getConverter($table, $name, 'persist', $query);
|
|
if ($converter !== null) {
|
|
$value = $this->$converter($value, $name, $table, $query);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Convert a value which was fetched from the data source
|
|
*
|
|
* @param string $table The table the value has been fetched from
|
|
* @param string $name The alias or column name
|
|
* @param mixed $value The value to convert
|
|
* @param RepositoryQuery $query An optional query to pass as context
|
|
* (Directly passed through to $this->getConverter)
|
|
*
|
|
* @return mixed If conversion was possible, the converted value,
|
|
* otherwise the unchanged value
|
|
*/
|
|
public function retrieveColumn($table, $name, $value, RepositoryQuery $query = null)
|
|
{
|
|
$converter = $this->getConverter($table, $name, 'retrieve', $query);
|
|
if ($converter !== null) {
|
|
$value = $this->$converter($value, $name, $table, $query);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Return the name of the conversion method for the given alias or column name and context
|
|
*
|
|
* @param string $table The datasource's table
|
|
* @param string $name The alias or column name for which to return a conversion method
|
|
* @param string $context The context of the conversion: persist or retrieve
|
|
* @param RepositoryQuery $query An optional query to pass as context
|
|
* (unused by the base implementation)
|
|
*
|
|
* @return string
|
|
*
|
|
* @throws ProgrammingError In case a conversion rule is found but not any conversion method
|
|
*/
|
|
protected function getConverter($table, $name, $context, RepositoryQuery $query = null)
|
|
{
|
|
$conversionRules = $this->getConversionRules();
|
|
if (! isset($conversionRules[$table])) {
|
|
return;
|
|
}
|
|
|
|
$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($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[$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[$alias]
|
|
);
|
|
}
|
|
|
|
Logger::debug(
|
|
'Conversion method "%s" for type definition "%s" does not exist in repository "%s".',
|
|
$context . $identifier,
|
|
$tableRules[$alias],
|
|
$this->getName()
|
|
);
|
|
} else {
|
|
return $context . $identifier;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Convert a timestamp or DateTime object to a string formatted using static::DATETIME_FORMAT
|
|
*
|
|
* @param mixed $value
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function persistDateTime($value)
|
|
{
|
|
if (is_numeric($value)) {
|
|
$value = date(static::DATETIME_FORMAT, $value);
|
|
} elseif ($value instanceof DateTime) {
|
|
$value = date(static::DATETIME_FORMAT, $value->getTimestamp()); // Using date here, to ignore any timezone
|
|
} elseif ($value !== null) {
|
|
throw new ProgrammingError(
|
|
'Cannot persist value "%s" as type date_time. It\'s not a timestamp or DateTime object',
|
|
$value
|
|
);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Convert a string formatted using static::DATETIME_FORMAT to a unix timestamp
|
|
*
|
|
* @param string $value
|
|
*
|
|
* @return int
|
|
*/
|
|
protected function retrieveDateTime($value)
|
|
{
|
|
if (is_numeric($value)) {
|
|
$value = (int) $value;
|
|
} elseif (is_string($value)) {
|
|
$dateTime = DateTime::createFromFormat(static::DATETIME_FORMAT, $value);
|
|
if ($dateTime === false) {
|
|
Logger::debug(
|
|
'Unable to parse string "%s" as type date_time with format "%s" in repository "%s"',
|
|
$value,
|
|
static::DATETIME_FORMAT,
|
|
$this->getName()
|
|
);
|
|
$value = null;
|
|
} else {
|
|
$value = $dateTime->getTimestamp();
|
|
}
|
|
} elseif ($value !== null) {
|
|
throw new ProgrammingError(
|
|
'Cannot retrieve value "%s" as type date_time. It\'s not a integer or (numeric) string',
|
|
$value
|
|
);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Convert the given array to an comma separated string
|
|
*
|
|
* @param array|string $value
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function persistCommaSeparatedString($value)
|
|
{
|
|
if (is_array($value)) {
|
|
$value = join(',', array_map('trim', $value));
|
|
} elseif ($value !== null && !is_string($value)) {
|
|
throw new ProgrammingError('Cannot persist value "%s" as comma separated string', $value);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Convert the given comma separated string to an array
|
|
*
|
|
* @param string $value
|
|
*
|
|
* @return array
|
|
*/
|
|
protected function retrieveCommaSeparatedString($value)
|
|
{
|
|
if ($value && is_string($value)) {
|
|
$value = String::trimSplit($value);
|
|
} elseif ($value !== null) {
|
|
throw new ProgrammingError('Cannot retrieve value "%s" as array. It\'s not a string', $value);
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Parse the given value based on the ASN.1 standard (GeneralizedTime) and return its timestamp representation
|
|
*
|
|
* @param string|null $value
|
|
*
|
|
* @return int
|
|
*/
|
|
protected function retrieveGeneralizedTime($value)
|
|
{
|
|
if ($value === null) {
|
|
return $value;
|
|
}
|
|
|
|
if (
|
|
($dateTime = DateTime::createFromFormat('YmdHis.uO', $value)) !== false
|
|
|| ($dateTime = DateTime::createFromFormat('YmdHis.uZ', $value)) !== false
|
|
|| ($dateTime = DateTime::createFromFormat('YmdHis.u', $value)) !== false
|
|
|| ($dateTime = DateTime::createFromFormat('YmdHis', $value)) !== false
|
|
|| ($dateTime = DateTime::createFromFormat('YmdHi', $value)) !== false
|
|
|| ($dateTime = DateTime::createFromFormat('YmdH', $value)) !== false
|
|
) {
|
|
return $dateTime->getTimeStamp();
|
|
} else {
|
|
Logger::debug(sprintf(
|
|
'Failed to parse "%s" based on the ASN.1 standard (GeneralizedTime) in repository "%s".',
|
|
$value,
|
|
$this->getName()
|
|
));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate that the requested table exists and resolve it's real name if necessary
|
|
*
|
|
* @param string $table The table to validate
|
|
* @param RepositoryQuery $query An optional query to pass as context
|
|
* (unused by the base implementation)
|
|
*
|
|
* @return string The table's name, may differ from the given one
|
|
*
|
|
* @throws ProgrammingError In case the given table does not exist
|
|
*/
|
|
public function requireTable($table, RepositoryQuery $query = null)
|
|
{
|
|
$queryColumns = $this->getQueryColumns();
|
|
if (! isset($queryColumns[$table])) {
|
|
throw new ProgrammingError('Table "%s" not found', $table);
|
|
}
|
|
|
|
$virtualTables = $this->getVirtualTables();
|
|
if (isset($virtualTables[$table])) {
|
|
$table = $virtualTables[$table];
|
|
}
|
|
|
|
return $table;
|
|
}
|
|
|
|
/**
|
|
* Recurse the given filter, require each column for the given table and convert all values
|
|
*
|
|
* @param string $table The table being filtered
|
|
* @param Filter $filter The filter to recurse
|
|
* @param RepositoryQuery $query An optional query to pass as context
|
|
* (Directly passed through to $this->requireFilterColumn)
|
|
* @param bool $clone Whether to clone $filter first
|
|
*
|
|
* @return Filter The udpated filter
|
|
*/
|
|
public function requireFilter($table, Filter $filter, RepositoryQuery $query = null, $clone = true)
|
|
{
|
|
if ($clone) {
|
|
$filter = clone $filter;
|
|
}
|
|
|
|
if ($filter->isExpression()) {
|
|
$column = $filter->getColumn();
|
|
$filter->setColumn($this->requireFilterColumn($table, $column, $query, $filter));
|
|
$filter->setExpression($this->persistColumn($table, $column, $filter->getExpression(), $query));
|
|
} elseif ($filter->isChain()) {
|
|
foreach ($filter->filters() as $chainOrExpression) {
|
|
$this->requireFilter($table, $chainOrExpression, $query, false);
|
|
}
|
|
}
|
|
|
|
return $filter;
|
|
}
|
|
|
|
/**
|
|
* Return this repository's query columns of the given table mapped to their respective aliases
|
|
*
|
|
* @param string $table
|
|
*
|
|
* @return array
|
|
*
|
|
* @throws ProgrammingError In case $table does not exist
|
|
*/
|
|
public function requireAllQueryColumns($table)
|
|
{
|
|
$queryColumns = $this->getQueryColumns();
|
|
if (! array_key_exists($table, $queryColumns)) {
|
|
throw new ProgrammingError('Table name "%s" not found', $table);
|
|
}
|
|
|
|
$blacklist = $this->getBlacklistedQueryColumns();
|
|
$columns = array();
|
|
foreach ($queryColumns[$table] as $alias => $column) {
|
|
if (! in_array(is_string($alias) ? $alias : $column, $blacklist)) {
|
|
$columns[$alias] = $this->resolveQueryColumnAlias($table, $alias);
|
|
}
|
|
}
|
|
|
|
return $columns;
|
|
}
|
|
|
|
/**
|
|
* Return the query column name for the given alias or null in case the alias does not exist
|
|
*
|
|
* @param string $table
|
|
* @param string $alias
|
|
*
|
|
* @return string|null
|
|
*/
|
|
public function resolveQueryColumnAlias($table, $alias)
|
|
{
|
|
$aliasColumnMap = $this->getAliasColumnMap();
|
|
if (isset($aliasColumnMap[$alias])) {
|
|
return $aliasColumnMap[$alias];
|
|
}
|
|
|
|
$prefixedAlias = $table . '.' . $alias;
|
|
if (isset($aliasColumnMap[$prefixedAlias])) {
|
|
return $aliasColumnMap[$prefixedAlias];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*
|
|
* @param string $table
|
|
* @param string $alias
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function validateQueryColumnAssociation($table, $alias)
|
|
{
|
|
$aliasTableMap = $this->getAliasTableMap();
|
|
if (isset($aliasTableMap[$alias])) {
|
|
return $aliasTableMap[$alias] === $table;
|
|
}
|
|
|
|
$columnTableMap = $this->getColumnTableMap();
|
|
if (isset($columnTableMap[$alias])) {
|
|
return $columnTableMap[$alias] === $table;
|
|
}
|
|
|
|
$prefixedAlias = $table . '.' . $alias;
|
|
return isset($aliasTableMap[$prefixedAlias]) || isset($columnTableMap[$prefixedAlias]);
|
|
}
|
|
|
|
/**
|
|
* Return whether the given column name or alias is a valid query column
|
|
*
|
|
* @param string $table The table where to look for the column or alias
|
|
* @param string $name The column name or alias to check
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasQueryColumn($table, $name)
|
|
{
|
|
if ($this->resolveQueryColumnAlias($table, $name) !== null) {
|
|
$alias = $name;
|
|
} elseif (($alias = $this->reassembleQueryColumnAlias($table, $name)) === null) {
|
|
return false;
|
|
}
|
|
|
|
return !in_array($alias, $this->getBlacklistedQueryColumns())
|
|
&& $this->validateQueryColumnAssociation($table, $name);
|
|
}
|
|
|
|
/**
|
|
* Validate that the given column is a valid query target and return it or the actual name if it's an alias
|
|
*
|
|
* @param string $table The table where to look for the column or alias
|
|
* @param string $name The name or alias of the column to validate
|
|
* @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation)
|
|
*
|
|
* @return string The given column's name
|
|
*
|
|
* @throws QueryException In case the given column is not a valid query column
|
|
*/
|
|
public function requireQueryColumn($table, $name, RepositoryQuery $query = 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 (in_array($alias, $this->getBlacklistedQueryColumns())) {
|
|
throw new QueryException(t('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);
|
|
}
|
|
|
|
return $column;
|
|
}
|
|
|
|
/**
|
|
* Return whether the given column name or alias is a valid filter column
|
|
*
|
|
* @param string $table The table where to look for the column or alias
|
|
* @param string $name The column name or alias to check
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasFilterColumn($table, $name)
|
|
{
|
|
return ($this->resolveQueryColumnAlias($table, $name) !== null
|
|
|| $this->reassembleQueryColumnAlias($table, $name) !== null)
|
|
&& $this->validateQueryColumnAssociation($table, $name);
|
|
}
|
|
|
|
/**
|
|
* Validate that the given column is a valid filter target and return it or the actual name if it's an alias
|
|
*
|
|
* @param string $table The table where to look for the column or alias
|
|
* @param string $name The name or alias of the column to validate
|
|
* @param RepositoryQuery $query An optional query to pass as context (unused by the base implementation)
|
|
* @param FilterExpression $filter An optional filter to pass as context (unused by the base implementation)
|
|
*
|
|
* @return string The given column's name
|
|
*
|
|
* @throws QueryException In case the given column is not a valid filter column
|
|
*/
|
|
public function requireFilterColumn($table, $name, RepositoryQuery $query = null, FilterExpression $filter = 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, $alias)) {
|
|
throw new QueryException(t('Filter column "%s" not found in table "%s"'), $name, $table);
|
|
}
|
|
|
|
return $column;
|
|
}
|
|
|
|
/**
|
|
* Return whether the given column name or alias of the given table is a valid statement column
|
|
*
|
|
* @param string $table The table where to look for the column or alias
|
|
* @param string $name The column name or alias to check
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function hasStatementColumn($table, $name)
|
|
{
|
|
return $this->hasQueryColumn($table, $name);
|
|
}
|
|
|
|
/**
|
|
* Validate that the given column is a valid statement column and return it or the actual name if it's an alias
|
|
*
|
|
* @param string $table The table for which to require the column
|
|
* @param string $name The name or alias of the column to validate
|
|
*
|
|
* @return string The given column's name
|
|
*
|
|
* @throws StatementException In case the given column is not a statement column
|
|
*/
|
|
public function requireStatementColumn($table, $name)
|
|
{
|
|
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 (in_array($alias, $this->getBlacklistedQueryColumns())) {
|
|
throw new StatementException('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);
|
|
}
|
|
|
|
return $column;
|
|
}
|
|
|
|
/**
|
|
* Resolve the given aliases or column names of the given table supposed to be persisted and convert their values
|
|
*
|
|
* @param string $table
|
|
* @param array $data
|
|
*
|
|
* @return array
|
|
*/
|
|
public function requireStatementColumns($table, array $data)
|
|
{
|
|
$resolved = array();
|
|
foreach ($data as $alias => $value) {
|
|
$resolved[$this->requireStatementColumn($table, $alias)] = $this->persistColumn($table, $alias, $value);
|
|
}
|
|
|
|
return $resolved;
|
|
}
|
|
}
|