*
  • Insert, update and delete capabilities
  • *
  • Triggers for inserts, updates and deletions
  • *
  • Lazy initialization of table specific configs
  • * */ 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 * * 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'] * ) * ) * * * @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 */ 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; } }