From 0d0fcc973b6dc087bbf1faf3df3f7560e538b971 Mon Sep 17 00:00:00 2001 From: Thomas Gelf Date: Fri, 24 Apr 2015 14:27:22 +0200 Subject: [PATCH] Initial tables and related base classes Still VERY simple fixes #9135 --- application/controllers/ListController.php | 39 ++ application/tables/IcingaCommandTable.php | 48 ++ application/tables/IcingaHostTable.php | 48 ++ application/tables/IcingaZoneTable.php | 40 ++ application/views/scripts/list/table.phtml | 8 + application/views/scripts/object/form.phtml | 7 + configuration.php | 12 + library/Director/Data/Db/DbObject.php | 716 ++++++++++++++++++++ library/Director/Web/Table/QuickTable.php | 84 +++ library/Director/Web/Table/TableLoader.php | 32 + 10 files changed, 1034 insertions(+) create mode 100644 application/controllers/ListController.php create mode 100644 application/tables/IcingaCommandTable.php create mode 100644 application/tables/IcingaHostTable.php create mode 100644 application/tables/IcingaZoneTable.php create mode 100644 application/views/scripts/list/table.phtml create mode 100644 application/views/scripts/object/form.phtml create mode 100644 configuration.php create mode 100644 library/Director/Data/Db/DbObject.php create mode 100644 library/Director/Web/Table/QuickTable.php create mode 100644 library/Director/Web/Table/TableLoader.php diff --git a/application/controllers/ListController.php b/application/controllers/ListController.php new file mode 100644 index 00000000..39cb4699 --- /dev/null +++ b/application/controllers/ListController.php @@ -0,0 +1,39 @@ +view->addLink = $this->view->qlink( + $this->translate('Add Host'), + 'director/object/host' + ); + $this->view->title = $this->translate('Icinga Hosts'); + $this->view->table = $this->loadTable('icingaHost')->setConnection($this->db()); + $this->render('table'); + } + + public function commandsAction() + { + $this->view->addLink = $this->view->qlink( + $this->translate('Add Command'), + 'director/object/command' + ); + $this->view->title = $this->translate('Icinga Commands'); + $this->view->table = $this->loadTable('icingaCommand')->setConnection($this->db()); + $this->render('table'); + } + + public function zonesAction() + { + $this->view->addLink = $this->view->qlink( + $this->translate('Add Zone'), + 'director/object/zone' + ); + $this->view->title = $this->translate('Icinga Zones'); + $this->view->table = $this->loadTable('icingaZone')->setConnection($this->db()); + $this->render('table'); + } +} diff --git a/application/tables/IcingaCommandTable.php b/application/tables/IcingaCommandTable.php new file mode 100644 index 00000000..7261d259 --- /dev/null +++ b/application/tables/IcingaCommandTable.php @@ -0,0 +1,48 @@ + 'c.id', + 'command' => 'c.object_name', + 'command_line' => 'c.command', + 'zone' => 'z.object_name', + ); + } + + protected function getActionLinks($id) + { + return $this->view()->qlink('Edit', 'director/object/command', array('id' => $id)); + } + + public function getTitles() + { + $view = $this->view(); + return array( + $view->translate('Command'), + $view->translate('Command line'), + $view->translate('Zone'), + ); + } + + public function fetchData() + { + $db = $this->connection()->getConnection(); + $query = $db->select()->from( + array('c' => 'icinga_command'), + $this->getColumns() + )->joinLeft( + array('z' => 'icinga_zone'), + 'c.zone_id = z.id', + array() + ); + + return $db->fetchAll($query); + } +} diff --git a/application/tables/IcingaHostTable.php b/application/tables/IcingaHostTable.php new file mode 100644 index 00000000..88a0e57d --- /dev/null +++ b/application/tables/IcingaHostTable.php @@ -0,0 +1,48 @@ + 'h.id', + 'host' => 'h.object_name', + 'address' => 'h.address', + 'zone' => 'z.object_name', + ); + } + + protected function getActionLinks($id) + { + return $this->view()->qlink('Edit', 'director/object/host', array('id' => $id)); + } + + public function getTitles() + { + $view = $this->view(); + return array( + 'host' => $view->translate('Hostname'), + 'address' => $view->translate('Address'), + 'zone' => $view->translate('Zone'), + ); + } + + public function fetchData() + { + $db = $this->connection()->getConnection(); + $query = $db->select()->from( + array('h' => 'icinga_host'), + $this->getColumns() + )->joinLeft( + array('z' => 'icinga_zone'), + 'h.zone_id = z.id', + array() + ); + + return $db->fetchAll($query); + } +} diff --git a/application/tables/IcingaZoneTable.php b/application/tables/IcingaZoneTable.php new file mode 100644 index 00000000..2a321fae --- /dev/null +++ b/application/tables/IcingaZoneTable.php @@ -0,0 +1,40 @@ + 'z.id', + 'zone' => 'z.object_name', + ); + } + + protected function getActionLinks($id) + { + return $this->view()->qlink('Edit', 'director/object/zone', array('id' => $id)); + } + + public function getTitles() + { + $view = $this->view(); + return array( + 'zone' => $view->translate('Zone'), + ); + } + + public function fetchData() + { + $db = $this->connection()->getConnection(); + $query = $db->select()->from( + array('z' => 'icinga_zone'), + $this->getColumns() + ); + + return $db->fetchAll($query); + } +} diff --git a/application/views/scripts/list/table.phtml b/application/views/scripts/list/table.phtml new file mode 100644 index 00000000..1ad5c254 --- /dev/null +++ b/application/views/scripts/list/table.phtml @@ -0,0 +1,8 @@ +
+

escape($this->title) ?>

+addLink ?> +
+ +
+table->render() ?> +
diff --git a/application/views/scripts/object/form.phtml b/application/views/scripts/object/form.phtml new file mode 100644 index 00000000..f3b5e8dc --- /dev/null +++ b/application/views/scripts/object/form.phtml @@ -0,0 +1,7 @@ +
+

escape($this->title) ?>

+
+ +
+form ?> +
diff --git a/configuration.php b/configuration.php new file mode 100644 index 00000000..915165bb --- /dev/null +++ b/configuration.php @@ -0,0 +1,12 @@ +menuSection($this->translate('Icinga Director')); + +$section->setIcon('cubes'); +$section->add($this->translate('Zones')) + ->setUrl('director/list/zones'); +$section->add($this->translate('Commands')) + ->setUrl('director/list/commands'); +$section->add($this->translate('Hosts')) + ->setUrl('director/list/hosts'); + diff --git a/library/Director/Data/Db/DbObject.php b/library/Director/Data/Db/DbObject.php new file mode 100644 index 00000000..c356ca7c --- /dev/null +++ b/library/Director/Data/Db/DbObject.php @@ -0,0 +1,716 @@ + + * @license GPLv2 http://www.gnu.org/licenses/gpl-2.0.html + */ +namespace Icinga\Module\Director\Data\Db; + +use Icinga\Data\Db\DbConnection; +use Exception; + +/** + * Base class for ... + */ +abstract class DbObject +{ + /** + * DbConnection + */ + protected $connection; + + /** + * Zend_Db_Adapter: DB Handle + */ + protected $db; + + /** + * Table name. MUST be set when extending this class + */ + protected $table; + + /** + * Default columns. MUST be set when extending this class. Each table + * column MUST be defined with a default value. Default value may be null. + */ + protected $defaultProperties; + + /** + * Properties as loaded from db + */ + protected $loadedProperties; + + /** + * Whether at least one property has been modified + */ + protected $hasBeenModified = false; + + /** + * Whether this object has been loaded from db + */ + protected $loadedFromDb = false; + + /** + * Object properties + */ + protected $properties = array(); + + /** + * Property names that have been modified since object creation + */ + protected $modifiedProperties = array(); + + /** + * Unique key name, could be primary + */ + protected $keyName; + + /** + * Set this to an eventual autoincrementing column. May equal $keyName + */ + protected $autoincKeyName; + + /** + * Constructor is not accessible and should not be overridden + */ + protected function __construct() + { + if ($this->table === null + || $this->keyName === null + || $this->defaultProperties === null + ) { + throw new Exception("Someone extending this class didn't RTFM"); + } + + $this->properties = $this->defaultProperties; + $this->beforeInit(); + } + + /** + * Kann überschrieben werden, um Kreuz-Checks usw vor dem Speichern durch- + * zuführen - die Funktion ist aber public und erlaubt jederzeit, die Kon- + * sistenz eines Objektes bei bedarf zu überprüfen. + * + * @return boolean Ob der Wert gültig ist + */ + public function validate() + { + return true; + } + + + /************************************************************************\ + * Nachfolgend finden sich ein paar Hooks, die bei Bedarf überschrieben * + * werden können. Wann immer möglich soll darauf verzichtet werden, * + * andere Funktionen (wie z.B. store()) zu überschreiben. * + \************************************************************************/ + + /** + * Wird ausgeführt, bevor die eigentlichen Initialisierungsoperationen + * (laden von Datenbank, aus Array etc) starten + * + * @return void + */ + protected function beforeInit() {} + + /** + * Wird ausgeführt, nachdem mittels ::factory() ein neues Objekt erstellt + * worden ist. + * + * @return void + */ + protected function onFactory() {} + + /** + * Wird ausgeführt, nachdem mittels ::factory() ein neues Objekt erstellt + * worden ist. + * + * @return void + */ + protected function onLoadFromDb() {} + + /** + * Wird ausgeführt, bevor ein Objekt abgespeichert wird. Die Operation + * wird aber auf jeden Fall durchgeführt, außer man wirft eine Exception + * + * @return void + */ + protected function beforeStore() {} + + /** + * Wird ausgeführt, nachdem ein Objekt erfolgreich gespeichert worden ist + * + * @return void + */ + protected function onStore() {} + + /** + * Wird ausgeführt, nachdem ein Objekt erfolgreich der Datenbank hinzu- + * gefügt worden ist + * + * @return void + */ + protected function onInsert() {} + + /** + * Wird ausgeführt, nachdem bestehendes Objekt erfolgreich der Datenbank + * geändert worden ist + * + * @return void + */ + protected function onUpdate() {} + + /** + * Wird ausgeführt, bevor ein Objekt gelöscht wird. Die Operation wird + * aber auf jeden Fall durchgeführt, außer man wirft eine Exception + * + * @return void + */ + protected function beforeDelete() {} + + /** + * Wird ausgeführt, nachdem bestehendes Objekt erfolgreich aud der + * Datenbank gelöscht worden ist + * + * @return void + */ + protected function onDelete() {} + + + /** + * Set DB adapter + * + * @param Zend_Db_Adapter $db DB adapter + * + * @return self + */ + public function setDb($db) + { + $this->db = $db; + return $this; + } + + /** + * Getter + * + * @param string $property Property + * + * @return mixed + */ + public function get($property) + { + $func = 'get' . ucfirst($property); + if (substr($func, -2) === '[]') { + $func = substr($func, 0, -2); + } + if (method_exists($this, $func)) { + return $this->$func(); + } + + if (! array_key_exists($property, $this->properties)) { + throw new Exception(sprintf('Trying to get invalid property "%s"', $property)); + } + return $this->properties[$property]; + } + + public function hasProperty($key) + { + if (array_key_exists($key, $this->properties)) { + return true; + } + $func = 'get' . ucfirst($key); + if (substr($func, -2) === '[]') { + $func = substr($func, 0, -2); + } + if (method_exists($this, $func)) { + return true; + } + return false; + } + + /** + * Generic setter + * + * @param string $property + * @param mixed $value + * + * @return array + */ + public function set($key, $value) + { + $key = (string) $key; + if ($value === '') { + $value = null; + } + if (! $this->hasProperty($key)) { + throw new Exception(sprintf('Trying to set invalid key %s', $key)); + } + $func = 'validate' . ucfirst($key); + if (method_exists($this, $func) && $this->$func($value) !== true) { + throw new Exception( + sprintf('Got invalid value "%s" for "%s"', $value, $key) + ); + } + $func = 'munge' . ucfirst($key); + if (method_exists($this, $func)) { + $value = $this->$func($value); + } + if ($value === $this->get($key)) { + return $this; + } + if ($key === $this->getKeyName() && $this->hasBeenLoadedFromDb()) { + throw new Exception('Changing primary key is not allowed'); + } + $func = 'set' . ucfirst($key); + if (substr($func, -2) === '[]') { + $func = substr($func, 0, -2); + } + if (method_exists($this, $func)) { + return $this->$func($value); + } + + return $this->reallySet($key, $value); + } + + protected function reallySet($key, $value) + { + if ($value === $this->$key) { + return $this; + } + $this->hasBeenModified = true; + $this->modifiedProperties[$key] = true; + $this->properties[$key] = $value; + return $this; + } + + /** + * Magic getter + * + * @return mixed + */ + public function __get($key) + { + return $this->get($key); + } + + /** + * Magic setter + * + * @param string $key Key + * @param mixed $val Value + * + * @return void + */ + public function __set($key, $val) + { + $this->set($key, $val); + } + + /** + * Magic isset check + * + * @return boolean + */ + public function __isset($key) + { + return array_key_exists($key, $this->properties); + } + + /** + * Magic unsetter + * + * @return void + */ + public function __unset($key) + { + if (! array_key_exists($key, $this->properties)) { + throw new Exception('Trying to unset invalid key'); + } + $this->properties[$key] = $this->defaultProperties[$key]; + } + + /** + * Führt die Operation set() für jedes Element (key/value Paare) der über- + * gebenen Arrays aus + * + * @param array $data Array mit den zu setzenden Daten + * @return self + */ + public function setProperties($props) + { + if (! is_array($props)) throw new Exception('Array required, got ' . gettype($props)); + foreach ($props as $key => $value) { + $this->set($key, $value); + } + return $this; + } + + /** + * Return an array with all object properties + * + * @return array + */ + public function getProperties() + { + return $this->properties; + } + + /** + * Return all properties that changed since object creation + * + * @return array + */ + public function getModifiedProperties() + { + $props = array(); + foreach (array_keys($this->modifiedProperties) as $key) { + if ($key === $this->keyName || $key === $this->autoincKeyName) continue; + $props[$key] = $this->properties[$key]; + } + return $props; + } + + /** + * Whether this object has been modified + * + * @return bool + */ + public function hasBeenModified() + { + return $this->hasBeenModified; + } + + /** + * Whether the given property has been modified + * + * @param string $key Property name + * @return boolean + */ + protected function hasModifiedProperty($key) + { + return array_key_exists($key, $this->modifiedProperties); + } + + /** + * Unique key name + * + * @return string + */ + public function getKeyName() + { + return $this->keyName; + } + + /** + * Autoinc key name + * + * @return string + */ + public function getAutoincKeyName() + { + return $this->autoincKeyName; + } + + /** + * Return the unique identifier + * + * @return string + */ + public function getId() + { + // TODO: Doesn't work for array() / multicol key + if (isset($this->properties[$this->keyName])) + { + return $this->properties[$this->keyName]; + } + return null; + } + + /** + * Get the autoinc value if set + * + * @return string + */ + public function getAutoincId() + { + if (isset($this->properties[$this->autoincKeyName])) + { + return $this->properties[$this->autoincKeyName]; + } + return null; + } + + /** + * Liefert das benutzte Datenbank-Handle + * + * @return Zend_Db_Adapter_Abstract + */ + public function getDb() + { + return $this->db; + } + + /** + * Lädt einen Datensatz aus der Datenbank und setzt die entsprechenden + * Eigenschaften dieses Objekts + * + * @return self + */ + protected function loadFromDb() + { + $select = $this->db->select()->from($this->table)->where($this->createWhere()); + $props = $this->db->fetchRow($select); + + if (empty($props)) { + $msg = sprintf('Got no "%s" data for: %s', $this->table, $this->getId()); + throw new Exception($msg); + } + + foreach ($props as $key => $val) { + if (! array_key_exists($key, $this->properties)) { + throw new Exception(sprintf( + 'Trying to set invalid %s key "%s". DB schema change?', + $this->table, + $key + )); + } + $this->properties[$key] = $val; + } + $this->loadedFromDb = true; + $this->loadedProperties = $this->properties; + $this->hasBeenModified = false; + return $this; + } + + public function hasBeenLoadedFromDb() + { + return $this->loadedFromDb; + } + + /** + * Ändert den entsprechenden Datensatz in der Datenbank + * + * @return int Anzahl der geänderten Zeilen + */ + protected function updateDb() + { + $properties = $this->getModifiedProperties(); + if (empty($properties)) { + // Fake true, we might have manually set this to "modified" + return true; + } + + // TODO: Remember changed data for audit and log + return $this->db->update( + $this->table, + $properties, + $this->createWhere() + ); + + } + + /** + * Fügt der Datenbank-Tabelle einen entsprechenden Datensatz hinzu + * + * @return int Anzahl der betroffenen Zeilen + */ + protected function insertIntoDb() + { + return $this->db->insert($this->table, $this->getProperties()); + } + + /** + * Store object to database + * + * @return boolean Whether storing succeeded + */ + public function store(DbConnection $db = null) + { + if ($db !== null) { + $this->connection = $db; + $this->db = $db->getConnection(); + } + + if ($this->validate() !== true) { + throw new Exception(sprintf( + '%s[%s] validation failed', + $this->table, + $this->getId() + )); + } + + if ($this->hasBeenLoadedFromDb() && ! $this->hasBeenModified()) { + return true; + } + + $this->beforeStore(); + $table = $this->table; + $id = $this->getId(); + $result = false; + + try { + if ($this->hasBeenLoadedFromDb()) { + if ($this->updateDb()) { + /*throw new Exception( + sprintf('%s "%s" has been modified', $table, $id) + );*/ + $result = true; + $this->onUpdate(); + } else { + throw new Exception( + sprintf('FAILED storing %s "%s"', $table, $id)); + } + } else { + if ($id && $this->existsInDb()) { + throw new Exception( + sprintf('Trying to recreate %s (%s)', $table, $id) + ); + } + + if ($this->insertIntoDb()) { + $id = $this->getId(); + if ($this->autoincKeyName) { + $this->properties[$this->autoincKeyName] = $this->db->lastInsertId(); + if (! $id) { + $id = '[' . $this->properties[$this->autoincKeyName] . ']'; + } + } + // $this->log(sprintf('New %s "%s" has been stored', $table, $id)); + $this->onInsert(); + $result = true; + } else { + throw new Exception( + sprintf('FAILED to store new %s "%s"', $table, $id) + ); + } + } + + } catch (Exception $e) { + throw new Exception( + sprintf( + 'Storing %s[%s] failed: %s {%s}', + $this->table, + $id, + $e->getMessage(), + print_r($this->getProperties(), 1) + ) + ); + } + $this->modifiedProperties = array(); + $this->hasBeenModified = false; + $this->onStore(); + $this->loadedFromDb = true; + return $result; + } + + + /** + * Delete item from DB + * + * @return int Affected rows + */ + protected function deleteFromDb() + { + return $this->db->delete( + $this->table, + $this->createWhere() + ); + } + + protected function setKey($key) + { + $keyname = $this->getKeyName(); + if (is_array($keyname)) { + foreach ($keyname as $idx => $k) { + $this->set($k, $key[$idx]); + } + } else { + $this->set($keyname, $key); + } + return $this; + } + + protected function existsInDb() + { + $result = $this->db->fetchRow( + $this->db->select()->from($this->table)->where($this->createWhere()) + ); + return $result !== false; + } + + protected function createWhere() + { + $key = $this->getKeyName(); + if (is_array($key) && ! is_empty($key)) { + $where = array(); + foreach($key as $k) { + $where[] = $this->db->quoteInto( + sprintf('%s = ?', $k), + $this->properties[$k] + ); + } + return implode(' AND ', $where); + } else { + return $this->db->quoteInto( + sprintf('%s = ?', $key), + $this->properties[$key] + ); + } + } + + public function delete() + { + $table = $this->table; + $id = $this->getId(); + if (! $this->hasBeenLoadedFromDb() || ! $this->existsInDb()) { + throw new Exception(sprintf('Cannot delete %s "%s" from Db', $table, $id)); + } + $this->beforeDelete(); + if (! $this->deleteFromDb()) { + throw new Exception(sprintf('Deleting %s (%s) FAILED', $table, $id)); + } + // $this->log(sprintf('%s "%s" has been DELETED', $table, $id)); + $this->onDelete(); + $this->loadedFromDb = false; + return true; + } + + public function __clone() + { + $this->autoincKeyName = null; + $this->loadedFromDb = false; + $this->hasBeenModified = true; + } + + public static function create($properties, DbConnection $connection = null) + { + $class = get_called_class(); + $obj = new $class(); + if ($connection !== null) { + $obj->connection = $connection; + $obj->setDb($connection->getDb()); + } + $obj->setProperties($properties); + return $obj; + } + + public static function load($id, DbConnection $connection) + { + $class = get_called_class(); + $obj = new $class(); + $obj->connection = $connection; + $obj->setDb($connection->getConnection())->setKey($id)->loadFromDb(); + return $obj; + } + + public static function exists($id, DbConnection $connection) + { + $class = get_called_class(); + $obj = new $class(); + $obj->connection = $connection; + $obj->setDb($connection->getDb())->setKey($id); + return $obj->existsInDb(); + } +} diff --git a/library/Director/Web/Table/QuickTable.php b/library/Director/Web/Table/QuickTable.php new file mode 100644 index 00000000..9ff8bc26 --- /dev/null +++ b/library/Director/Web/Table/QuickTable.php @@ -0,0 +1,84 @@ +\n"; + $idKey = key($row); + $id = $row->$idKey; + unset($row->$idKey); + + foreach ($row as $key => $val) { + $htm .= ' ' . ($val === null ? '-' : $this->view()->escape($val)) . "\n"; + } + $htm .= ' ' . $this->getActionLinks($id) . "\n"; + return $htm . " \n"; + } + + public function setConnection(Selectable $connection) + { + $this->connection = $connection; + return $this; + } + + protected function connection() + { + // TODO: Fail if missing? Require connection in constructor? + return $this->connection; + } + + protected function renderTitles($row) + { + $view = $this->view; + $htm = "\n \n"; + foreach ($row as $title) { + $htm .= ' ' . $view->escape($title) . "\n"; + } + $htm .= ' ' . $view->translate('Actions') . "\n"; + return $htm . " \n\n"; + } + + public function render() + { + $data = $this->fetchData(); + + $htm = '' . "\n" + . $this->renderTitles($this->getTitles()) + . "\n"; + foreach ($data as $row) { + $htm .= $this->renderRow($row); + } + return $htm . "\n
\n"; + } + + protected function view() + { + if ($this->view === null) { + $this->view = Icinga::app()->getViewRenderer()->view; + } + return $this->view; + } + + + public function setView($view) + { + $this->view = $view; + } + + public function __toString() + { + return $this->render(); + } +} diff --git a/library/Director/Web/Table/TableLoader.php b/library/Director/Web/Table/TableLoader.php new file mode 100644 index 00000000..a9b76da4 --- /dev/null +++ b/library/Director/Web/Table/TableLoader.php @@ -0,0 +1,32 @@ +getApplicationDir('tables'); + $ns = '\\Icinga\\Web\\Tables\\'; + } else { + $basedir = $module->getBaseDir() . '/application/tables'; + $ns = '\\Icinga\\Module\\' . ucfirst($module->getName()) . '\\Tables\\'; + } + if (preg_match('~^[a-z0-9/]+$~i', $name)) { + $parts = preg_split('~/~', $name); + $class = ucfirst(array_pop($parts)) . 'Table'; + $file = sprintf('%s/%s/%s.php', rtrim($basedir, '/'), implode('/', $parts), $class); + if (file_exists($file)) { + require_once($file); + $class = $ns . $class; + return new $class(); + } + } + throw new ProgrammingError(sprintf('Cannot load %s (%s), no such table', $name, $file)); + } +}