diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php new file mode 100644 index 000000000..7953422be --- /dev/null +++ b/library/Icinga/Repository/IniRepository.php @@ -0,0 +1,201 @@ + + *
  • Insert, update and delete capabilities
  • + * + */ +abstract class IniRepository extends Repository implements Extensible, Updatable, Reducible +{ + /** + * Insert the given data for the given target + * + * In case the data source provides a valid key column, $data must provide a proper + * value for it which is then being used as the section name instead of $target. + * + * @param string $target + * @param array $data + * + * @throws StatementException In case the operation has failed + */ + public function insert($target, array $data) + { + $newData = $this->requireStatementColumns($data); + $section = $this->extractSectionName($target, $newData); + + if ($this->ds->hasSection($section)) { + throw new StatementException(t('Cannot insert. Section "%s" does already exist'), $section); + } + + $this->ds->setSection($section, $newData); + + try { + $this->ds->saveIni(); + } catch (Exception $e) { + throw new StatementException(t('Failed to insert. An error occurred: %s'), $e->getMessage()); + } + } + + /** + * Update the target with the given data and optionally limit the affected entries by using a filter + * + * The section(s) to update are either identified by $filter or $target, in order. If neither of both + * is given, all sections provided by the data source are going to be updated. Uniqueness of a section's + * name will be ensured. + * + * @param string $target + * @param array $data + * @param Filter $filter + * + * @throws StatementException In case the operation has failed + */ + public function update($target, array $data, Filter $filter = null) + { + $newData = $this->requireStatementColumns($data); + $keyColumn = $this->ds->getConfigObject()->getKeyColumn(); + if ($keyColumn && $filter === null && isset($newData[$keyColumn]) && !$this->ds->hasSection($target)) { + throw new StatementException( + t('Cannot update. Column "%s" holds a section\'s name which must be unique'), + $keyColumn + ); + } + + if ($target && !$filter) { + if (! $this->ds->hasSection($target)) { + throw new StatementException(t('Cannot update. Section "%s" does not exist'), $target); + } + + $results = array($target => $this->ds->getSection($target)); + } else { + $query = $this->ds->select(); + if ($filter) { + $this->requireFilter($filter); + $query->applyFilter($filter); + } + + $results = $query->fetchAll(); + } + + $newSection = null; + foreach ($results as $section => $config) { + if ($newSection !== null) { + throw new StatementException( + t('Cannot update. Column "%s" holds a section\'s name which must be unique'), + $keyColumn + ); + } + + foreach ($newData as $column => $value) { + if ($keyColumn && $column === $keyColumn) { + $newSection = $value; + } else { + $config->$column = $value; + } + } + + if ($keyColumn && isset($config->$keyColumn) && $config->$keyColumn === $section) { + unset($config->$keyColumn); + } + + if ($newSection) { + if ($this->ds->hasSection($newSection)) { + throw new StatementException(t('Cannot update. Section "%s" does already exist'), $newSection); + } + + $this->ds->removeSection($section)->setSection($newSection, $config); + } else { + $this->ds->setSection($section, $config); + } + } + + try { + $this->ds->saveIni(); + } catch (Exception $e) { + throw new StatementException(t('Failed to update. An error occurred: %s'), $e->getMessage()); + } + } + + /** + * Delete entries in the given target, optionally limiting the affected entries by using a filter + * + * The section(s) to delete are either identified by $filter or $target, in order. If neither of both + * is given, all sections provided by the data source are going to be deleted. + * + * @param string $target + * @param Filter $filter + * + * @throws StatementException In case the operation has failed + */ + public function delete($target, Filter $filter = null) + { + if ($target && !$filter) { + if (! $this->ds->hasSection($target)) { + return; // Nothing to do + } + + $results = array($target => $this->ds->getSection($target)); + } else { + $query = $this->ds->select(); + if ($filter) { + $this->requireFilter($filter); + $query->applyFilter($filter); + } + + $results = $query->fetchAll(); + } + + foreach ($results as $section => $_) { + $this->ds->removeSection($section); + } + + try { + $this->ds->saveIni(); + } catch (Exception $e) { + throw new StatementException(t('Failed to delete. An error occurred: %s'), $e->getMessage()); + } + } + + /** + * Extract and return the section name off of the given $data, if available, or validate $target + * + * @param string $target + * @param array $data + * + * @return string + * + * @throws ProgrammingError In case no valid section name is available + */ + protected function extractSectionName($target, array & $data) + { + if (($keyColumn = $this->ds->getConfigObject()->getKeyColumn())) { + if (! isset($data[$keyColumn])) { + throw new ProgrammingError('$data does not provide a value for key column "%s"', $keyColumn); + } + + $target = $data[$keyColumn]; + unset($data[$keyColumn]); + } + + if (! is_string($target)) { + throw new ProgrammingError( + 'Neither the data source nor the $target parameter provide a valid section name' + ); + } + + return $target; + } +} diff --git a/library/Icinga/Repository/Repository.php b/library/Icinga/Repository/Repository.php index 3ecdbe61c..1726426ba 100644 --- a/library/Icinga/Repository/Repository.php +++ b/library/Icinga/Repository/Repository.php @@ -8,6 +8,7 @@ use Icinga\Data\Filter\Filter; use Icinga\Data\Selectable; use Icinga\Exception\ProgrammingError; use Icinga\Exception\QueryException; +use Icinga\Exception\StatementException; /** * Abstract base class for concrete repository implementations @@ -573,4 +574,56 @@ abstract class Repository implements Selectable return $aliasColumnMap[$name]; } + + /** + * Return whether the given column name or alias is a valid statement column + * + * @param string $name The column name or alias to check + * + * @return bool + */ + public function hasStatementColumn($name) + { + return $this->hasQueryColumn($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 $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($name) + { + if (in_array($name, $this->filterColumns)) { + throw new StatementException('Filter column "%s" cannot be referenced in a statement', $name); + } + + $aliasColumnMap = $this->getAliasColumnMap(); + if (! array_key_exists($name, $aliasColumnMap)) { + throw new StatementException('Statement column "%s" not found', $name); + } + + return $aliasColumnMap[$name]; + } + + /** + * Resolve the given aliases or column names supposed to be persisted and convert their values + * + * @param array $data + * + * @return array + */ + public function requireStatementColumns(array $data) + { + $resolved = array(); + foreach ($data as $alias => $value) { + $resolved[$this->requireStatementColumn($alias)] = $this->persistColumn($alias, $value); + } + + return $resolved; + } }