summaryrefslogtreecommitdiffstats
path: root/library/Icinga/Repository/IniRepository.php
diff options
context:
space:
mode:
Diffstat (limited to 'library/Icinga/Repository/IniRepository.php')
-rw-r--r--library/Icinga/Repository/IniRepository.php420
1 files changed, 420 insertions, 0 deletions
diff --git a/library/Icinga/Repository/IniRepository.php b/library/Icinga/Repository/IniRepository.php
new file mode 100644
index 0000000..7385b3e
--- /dev/null
+++ b/library/Icinga/Repository/IniRepository.php
@@ -0,0 +1,420 @@
+<?php
+/* Icinga Web 2 | (c) 2015 Icinga Development Team | GPLv2+ */
+
+namespace Icinga\Repository;
+
+use Exception;
+use Icinga\Application\Config;
+use Icinga\Data\ConfigObject;
+use Icinga\Data\Extensible;
+use Icinga\Data\Filter\Filter;
+use Icinga\Data\Updatable;
+use Icinga\Data\Reducible;
+use Icinga\Exception\ProgrammingError;
+use Icinga\Exception\StatementException;
+
+/**
+ * Abstract base class for concrete INI repository implementations
+ *
+ * Additionally provided features:
+ * <ul>
+ * <li>Insert, update and delete capabilities</li>
+ * <li>Triggers for inserts, updates and deletions</li>
+ * <li>Lazy initialization of table specific configs</li>
+ * </ul>
+ */
+abstract class IniRepository extends Repository implements Extensible, Updatable, Reducible
+{
+ /**
+ * The configuration files used as table specific datasources
+ *
+ * This must be initialized by concrete repository implementations, in the following format
+ * <code>
+ * array(
+ * 'table_name' => array(
+ * 'name' => 'name_of_the_ini_file_without_extension',
+ * 'keyColumn' => 'the_name_of_the_column_to_use_as_key_column',
+ * ['module' => 'the_name_of_the_module_if_any']
+ * )
+ * )
+ * </code>
+ *
+ * @var array
+ */
+ protected $configs;
+
+ /**
+ * The tables for which triggers are available when inserting, updating or deleting rows
+ *
+ * This may be initialized by concrete repository implementations and describes for which table names triggers
+ * are available. The repository attempts to find a method depending on the type of event and table for which
+ * to run the trigger. The name of such a method is expected to be declared using lowerCamelCase.
+ * (e.g. group_membership will be translated to onUpdateGroupMembership and groupmembership will be translated
+ * to onUpdateGroupmembership) The available events are onInsert, onUpdate and onDelete.
+ *
+ * @var array
+ */
+ protected $triggers;
+
+ /**
+ * Create a new INI repository object
+ *
+ * @param Config|null $ds The data source to use
+ *
+ * @throws ProgrammingError In case the given data source does not provide a valid key column
+ */
+ public function __construct(Config $ds = null)
+ {
+ parent::__construct($ds); // First! Due to init().
+
+ if ($ds !== null && !$ds->getConfigObject()->getKeyColumn()) {
+ throw new ProgrammingError('INI repositories require their data source to provide a valid key column');
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @return Config
+ */
+ public function getDataSource($table = null)
+ {
+ if ($this->ds !== null) {
+ return parent::getDataSource($table);
+ }
+
+ $table = $table ?: $this->getBaseTable();
+ $configs = $this->getConfigs();
+ if (! isset($configs[$table])) {
+ throw new ProgrammingError('Config for table "%s" missing', $table);
+ } elseif (! $configs[$table] instanceof Config) {
+ $configs[$table] = $this->createConfig($configs[$table], $table);
+ }
+
+ if (! $configs[$table]->getConfigObject()->getKeyColumn()) {
+ throw new ProgrammingError(
+ 'INI repositories require their data source to provide a valid key column'
+ );
+ }
+
+ return $configs[$table];
+ }
+
+ /**
+ * Return the configuration files used as table specific datasources
+ *
+ * Calls $this->initializeConfigs() in case $this->configs is null.
+ *
+ * @return array
+ */
+ public function getConfigs()
+ {
+ if ($this->configs === null) {
+ $this->configs = $this->initializeConfigs();
+ }
+
+ return $this->configs;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the configs lazily
+ *
+ * @return array
+ */
+ protected function initializeConfigs()
+ {
+ return array();
+ }
+
+ /**
+ * Return the tables for which triggers are available when inserting, updating or deleting rows
+ *
+ * Calls $this->initializeTriggers() in case $this->triggers is null.
+ *
+ * @return array
+ */
+ public function getTriggers()
+ {
+ if ($this->triggers === null) {
+ $this->triggers = $this->initializeTriggers();
+ }
+
+ return $this->triggers;
+ }
+
+ /**
+ * Overwrite this in your repository implementation in case you need to initialize the triggers lazily
+ *
+ * @return array
+ */
+ protected function initializeTriggers()
+ {
+ return array();
+ }
+
+ /**
+ * Run a trigger for the given table and row which is about to be inserted
+ *
+ * @param string $table
+ * @param ConfigObject $new
+ *
+ * @return ConfigObject
+ */
+ public function onInsert($table, ConfigObject $new)
+ {
+ $trigger = $this->getTrigger($table, 'onInsert');
+ if ($trigger !== null) {
+ $row = $this->$trigger($new);
+ if ($row !== null) {
+ $new = $row;
+ }
+ }
+
+ return $new;
+ }
+
+ /**
+ * Run a trigger for the given table and row which is about to be updated
+ *
+ * @param string $table
+ * @param ConfigObject $old
+ * @param ConfigObject $new
+ *
+ * @return ConfigObject
+ */
+ public function onUpdate($table, ConfigObject $old, ConfigObject $new)
+ {
+ $trigger = $this->getTrigger($table, 'onUpdate');
+ if ($trigger !== null) {
+ $row = $this->$trigger($old, $new);
+ if ($row !== null) {
+ $new = $row;
+ }
+ }
+
+ return $new;
+ }
+
+ /**
+ * Run a trigger for the given table and row which has been deleted
+ *
+ * @param string $table
+ * @param ConfigObject $old
+ *
+ * @return ConfigObject
+ */
+ public function onDelete($table, ConfigObject $old)
+ {
+ $trigger = $this->getTrigger($table, 'onDelete');
+ if ($trigger !== null) {
+ $this->$trigger($old);
+ }
+ }
+
+ /**
+ * Return the name of the trigger method for the given table and event-type
+ *
+ * @param string $table The table name for which to return a trigger method
+ * @param string $event The name of the event type
+ *
+ * @return string
+ */
+ protected function getTrigger($table, $event)
+ {
+ if (! in_array($table, $this->getTriggers())) {
+ return;
+ }
+
+ $identifier = join('', array_map('ucfirst', explode('_', $table)));
+ if (method_exists($this, $event . $identifier)) {
+ return $event . $identifier;
+ }
+ }
+
+ /**
+ * Insert the given data for the given target
+ *
+ * $data must provide a proper value for the data source's key column.
+ *
+ * @param string $target
+ * @param array $data
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function insert($target, array $data)
+ {
+ $ds = $this->getDataSource($target);
+ $newData = $this->requireStatementColumns($target, $data);
+
+ $config = $this->onInsert($target, new ConfigObject($newData));
+ $section = $this->extractSectionName($config, $ds->getConfigObject()->getKeyColumn());
+
+ if ($ds->hasSection($section)) {
+ throw new StatementException(t('Cannot insert. Section "%s" does already exist'), $section);
+ }
+
+ $ds->setSection($section, $config);
+
+ try {
+ $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
+ *
+ * @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)
+ {
+ $ds = $this->getDataSource($target);
+ $newData = $this->requireStatementColumns($target, $data);
+
+ $keyColumn = $ds->getConfigObject()->getKeyColumn();
+ if ($filter === null && isset($newData[$keyColumn])) {
+ throw new StatementException(
+ t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
+ $keyColumn
+ );
+ }
+
+ $query = $ds->select();
+ if ($filter !== null) {
+ $query->addFilter($this->requireFilter($target, $filter));
+ }
+
+ /** @var ConfigObject $config */
+ $newSection = null;
+ foreach ($query as $section => $config) {
+ if ($newSection !== null) {
+ throw new StatementException(
+ t('Cannot update. Column "%s" holds a section\'s name which must be unique'),
+ $keyColumn
+ );
+ }
+
+ $newConfig = clone $config;
+ foreach ($newData as $column => $value) {
+ if ($column === $keyColumn) {
+ if ($value !== $config->get($keyColumn)) {
+ $newSection = $value;
+ }
+ } else {
+ $newConfig->$column = $value;
+ }
+ }
+
+ // This is necessary as the query result set contains the key column.
+ unset($newConfig->$keyColumn);
+
+ if ($newSection) {
+ if ($ds->hasSection($newSection)) {
+ throw new StatementException(t('Cannot update. Section "%s" does already exist'), $newSection);
+ }
+
+ $ds->removeSection($section)->setSection(
+ $newSection,
+ $this->onUpdate($target, $config, $newConfig)
+ );
+ } else {
+ $ds->setSection(
+ $section,
+ $this->onUpdate($target, $config, $newConfig)
+ );
+ }
+ }
+
+ try {
+ $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
+ *
+ * @param string $target
+ * @param Filter $filter
+ *
+ * @throws StatementException In case the operation has failed
+ */
+ public function delete($target, Filter $filter = null)
+ {
+ $ds = $this->getDataSource($target);
+
+ $query = $ds->select();
+ if ($filter !== null) {
+ $query->addFilter($this->requireFilter($target, $filter));
+ }
+
+ /** @var ConfigObject $config */
+ foreach ($query as $section => $config) {
+ $ds->removeSection($section);
+ $this->onDelete($target, $config);
+ }
+
+ try {
+ $ds->saveIni();
+ } catch (Exception $e) {
+ throw new StatementException(t('Failed to delete. An error occurred: %s'), $e->getMessage());
+ }
+ }
+
+ /**
+ * Create and return a Config for the given meta and table
+ *
+ * @param array $meta
+ * @param string $table
+ *
+ * @return Config
+ *
+ * @throws ProgrammingError In case the given meta is invalid
+ */
+ protected function createConfig(array $meta, $table)
+ {
+ if (! isset($meta['name'])) {
+ throw new ProgrammingError('Config file name missing for table "%s"', $table);
+ } elseif (! isset($meta['keyColumn'])) {
+ throw new ProgrammingError('Config key column name missing for table "%s"', $table);
+ }
+
+ if (isset($meta['module'])) {
+ $config = Config::module($meta['module'], $meta['name']);
+ } else {
+ $config = Config::app($meta['name']);
+ }
+
+ $config->getConfigObject()->setKeyColumn($meta['keyColumn']);
+ return $config;
+ }
+
+ /**
+ * Extract and return the section name off of the given $config
+ *
+ * @param array|ConfigObject $config
+ * @param string $keyColumn
+ *
+ * @return string
+ *
+ * @throws ProgrammingError In case no valid section name is available
+ */
+ protected function extractSectionName(&$config, $keyColumn)
+ {
+ if (! is_array($config) && !$config instanceof ConfigObject) {
+ throw new ProgrammingError('$config is neither an array nor a ConfigObject');
+ } elseif (! isset($config[$keyColumn])) {
+ throw new ProgrammingError('$config does not provide a value for key column "%s"', $keyColumn);
+ }
+
+ $section = $config[$keyColumn];
+ unset($config[$keyColumn]);
+ return $section;
+ }
+}