421 lines
13 KiB
PHP
421 lines
13 KiB
PHP
<?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;
|
|
}
|
|
}
|