From 31ae116fe4ab5cac3d2a85069adae2d211ecc846 Mon Sep 17 00:00:00 2001 From: Bernhard Posselt Date: Tue, 8 Apr 2014 18:50:10 +0200 Subject: migrated database, utility, bootstrap from appframework --- db/doesnotexistexception.php | 42 +++++ db/entity.php | 229 +++++++++++++++++++++++++ db/feed.php | 2 - db/feedmapper.php | 2 - db/folder.php | 2 - db/foldermapper.php | 2 - db/iapi.php | 3 - db/item.php | 2 - db/itemmapper.php | 3 - db/mapper.php | 285 ++++++++++++++++++++++++++++++++ db/multipleobjectsreturnedexception.php | 42 +++++ db/postgres/itemmapper.php | 6 +- 12 files changed, 601 insertions(+), 19 deletions(-) create mode 100644 db/doesnotexistexception.php create mode 100644 db/entity.php create mode 100644 db/mapper.php create mode 100644 db/multipleobjectsreturnedexception.php (limited to 'db') diff --git a/db/doesnotexistexception.php b/db/doesnotexistexception.php new file mode 100644 index 000000000..a6cdaa19d --- /dev/null +++ b/db/doesnotexistexception.php @@ -0,0 +1,42 @@ +. + * + */ + + +namespace OCA\News\Db; + + +/** + * This is returned or should be returned when a find request does not find an + * entry in the database + */ +class DoesNotExistException extends \Exception { + + /** + * Constructor + * @param string $msg the error message + */ + public function __construct($msg){ + parent::__construct($msg); + } + +} \ No newline at end of file diff --git a/db/entity.php b/db/entity.php new file mode 100644 index 000000000..7e1075027 --- /dev/null +++ b/db/entity.php @@ -0,0 +1,229 @@ +. +* +*/ + +namespace OCA\News\Db; + + +abstract class Entity { + + public $id; + + private $_updatedFields = array(); + private $_fieldTypes = array('id' => 'integer'); + + + /** + * Simple alternative constructor for building entities from a request + * @param array $params the array which was obtained via $this->params('key') + * in the controller + * @return Entity + */ + public static function fromParams(array $params) { + $instance = new static(); + + foreach($params as $key => $value) { + $method = 'set' . ucfirst($key); + $instance->$method($value); + } + + return $instance; + } + + + /** + * Maps the keys of the row array to the attributes + * @param array $row the row to map onto the entity + */ + public static function fromRow(array $row){ + $instance = new static(); + + foreach($row as $key => $value){ + $prop = ucfirst($instance->columnToProperty($key)); + $setter = 'set' . $prop; + $instance->$setter($value); + } + + $instance->resetUpdatedFields(); + + return $instance; + } + + + /** + * @return an array with attribute and type + */ + public function getFieldTypes() { + return $this->_fieldTypes; + } + + + /** + * Marks the entity as clean needed for setting the id after the insertion + */ + public function resetUpdatedFields(){ + $this->_updatedFields = array(); + } + + + protected function setter($name, $args) { + // setters should only work for existing attributes + if(property_exists($this, $name)){ + $this->markFieldUpdated($name); + + // if type definition exists, cast to correct type + if($args[0] !== null && array_key_exists($name, $this->_fieldTypes)) { + settype($args[0], $this->_fieldTypes[$name]); + } + $this->$name = $args[0]; + + } else { + throw new \BadFunctionCallException($name . + ' is not a valid attribute'); + } + } + + + protected function getter($name) { + // getters should only work for existing attributes + if(property_exists($this, $name)){ + return $this->$name; + } else { + throw new \BadFunctionCallException($name . + ' is not a valid attribute'); + } + } + + + /** + * Each time a setter is called, push the part after set + * into an array: for instance setId will save Id in the + * updated fields array so it can be easily used to create the + * getter method + */ + public function __call($methodName, $args){ + $attr = lcfirst( substr($methodName, 3) ); + + if(strpos($methodName, 'set') === 0){ + $this->setter($attr, $args); + } elseif(strpos($methodName, 'get') === 0) { + return $this->getter($attr); + } else { + throw new \BadFunctionCallException($methodName . + ' does not exist'); + } + + } + + + /** + * Mark am attribute as updated + * @param string $attribute the name of the attribute + */ + protected function markFieldUpdated($attribute){ + $this->_updatedFields[$attribute] = true; + } + + + /** + * Transform a database columnname to a property + * @param string $columnName the name of the column + * @return string the property name + */ + public function columnToProperty($columnName){ + $parts = explode('_', $columnName); + $property = null; + + foreach($parts as $part){ + if($property === null){ + $property = $part; + } else { + $property .= ucfirst($part); + } + } + + return $property; + } + + + /** + * Transform a property to a database column name + * @param string $property the name of the property + * @return string the column name + */ + public function propertyToColumn($property){ + $parts = preg_split('/(?=[A-Z])/', $property); + $column = null; + + foreach($parts as $part){ + if($column === null){ + $column = $part; + } else { + $column .= '_' . lcfirst($part); + } + } + + return $column; + } + + + /** + * @return array array of updated fields for update query + */ + public function getUpdatedFields(){ + return $this->_updatedFields; + } + + + /** + * Adds type information for a field so that its automatically casted to + * that value once its being returned from the database + * @param string $fieldName the name of the attribute + * @param string $type the type which will be used to call settype() + */ + protected function addType($fieldName, $type){ + $this->_fieldTypes[$fieldName] = $type; + } + + + /** + * Slugify the value of a given attribute + * Warning: This doesn't result in a unique value + * @param string $attributeName the name of the attribute, which value should be slugified + * @return string slugified value + */ + public function slugify($attributeName){ + // toSlug should only work for existing attributes + if(property_exists($this, $attributeName)){ + $value = $this->$attributeName; + // replace everything except alphanumeric with a single '-' + $value = preg_replace('/[^A-Za-z0-9]+/', '-', $value); + $value = strtolower($value); + // trim '-' + return trim($value, '-'); + } else { + throw new \BadFunctionCallException($attributeName . + ' is not a valid attribute'); + } + } + +} diff --git a/db/feed.php b/db/feed.php index 668e9a041..4d7318842 100644 --- a/db/feed.php +++ b/db/feed.php @@ -25,8 +25,6 @@ namespace OCA\News\Db; -use \OCA\AppFramework\Db\Entity; - class Feed extends Entity implements IAPI { diff --git a/db/feedmapper.php b/db/feedmapper.php index 8d82388de..ef2a98f8c 100644 --- a/db/feedmapper.php +++ b/db/feedmapper.php @@ -26,8 +26,6 @@ namespace OCA\News\Db; use \OCA\AppFramework\Core\API; -use \OCA\AppFramework\Db\Mapper; -use \OCA\AppFramework\Db\Entity; class FeedMapper extends Mapper implements IMapper { diff --git a/db/folder.php b/db/folder.php index e344aaecd..4e388cbe5 100644 --- a/db/folder.php +++ b/db/folder.php @@ -25,8 +25,6 @@ namespace OCA\News\Db; -use \OCA\AppFramework\Db\Entity; - class Folder extends Entity implements IAPI { diff --git a/db/foldermapper.php b/db/foldermapper.php index 8859268e0..f44ff3905 100644 --- a/db/foldermapper.php +++ b/db/foldermapper.php @@ -26,8 +26,6 @@ namespace OCA\News\Db; use \OCA\AppFramework\Core\API; -use \OCA\AppFramework\Db\Mapper; -use \OCA\AppFramework\Db\Entity; class FolderMapper extends Mapper implements IMapper { diff --git a/db/iapi.php b/db/iapi.php index 1c5a767ab..2f4043641 100644 --- a/db/iapi.php +++ b/db/iapi.php @@ -25,9 +25,6 @@ namespace OCA\News\Db; -use \OCA\AppFramework\Db\Entity; - - interface IAPI { public function toAPI(); } diff --git a/db/item.php b/db/item.php index 6f2ee9e6f..141c0b8a6 100644 --- a/db/item.php +++ b/db/item.php @@ -25,8 +25,6 @@ namespace OCA\News\Db; -use \OCA\AppFramework\Db\Entity; - class Item extends Entity implements IAPI { diff --git a/db/itemmapper.php b/db/itemmapper.php index b01acfbb9..8da5d27e1 100644 --- a/db/itemmapper.php +++ b/db/itemmapper.php @@ -24,9 +24,6 @@ namespace OCA\News\Db; -use \OCA\AppFramework\Db\DoesNotExistException; -use \OCA\AppFramework\Db\MultipleObjectsReturnedException; -use \OCA\AppFramework\Db\Mapper; use \OCA\AppFramework\Core\API; class ItemMapper extends Mapper implements IMapper { diff --git a/db/mapper.php b/db/mapper.php new file mode 100644 index 000000000..a0988bd54 --- /dev/null +++ b/db/mapper.php @@ -0,0 +1,285 @@ +. + * + */ + + +namespace OCA\News\Db; + +use OCA\AppFramework\Core\API; + + +/** + * Simple parent class for inheriting your data access layer from. This class + * may be subject to change in the future + */ +abstract class Mapper { + + protected $tableName; + protected $entityClass; + + /** + * @param API $api Instance of the API abstraction layer + * @param string $tableName the name of the table. set this to allow entity + * @param string $entityClass the name of the entity that the sql should be + * mapped to queries without using sql + */ + public function __construct(API $api, $tableName, $entityClass=null){ + $this->api = $api; + $this->tableName = '*PREFIX*' . $tableName; + + // if not given set the entity name to the class without the mapper part + // cache it here for later use since reflection is slow + if($entityClass === null) { + $this->entityClass = str_replace('Mapper', '', get_class($this)); + } else { + $this->entityClass = $entityClass; + } + } + + + /** + * @return string the table name + */ + public function getTableName(){ + return $this->tableName; + } + + + /** + * Deletes an entity from the table + * @param Entity $entity the entity that should be deleted + */ + public function delete(Entity $entity){ + $sql = 'DELETE FROM `' . $this->tableName . '` WHERE `id` = ?'; + $this->execute($sql, array($entity->getId())); + } + + + /** + * Creates a new entry in the db from an entity + * @param Entity $enttiy the entity that should be created + * @return the saved entity with the set id + */ + public function insert(Entity $entity){ + // get updated fields to save, fields have to be set using a setter to + // be saved + $properties = $entity->getUpdatedFields(); + $values = ''; + $columns = ''; + $params = array(); + + // build the fields + $i = 0; + foreach($properties as $property => $updated) { + $column = $entity->propertyToColumn($property); + $getter = 'get' . ucfirst($property); + + $columns .= '`' . $column . '`'; + $values .= '?'; + + // only append colon if there are more entries + if($i < count($properties)-1){ + $columns .= ','; + $values .= ','; + } + + array_push($params, $entity->$getter()); + $i++; + + } + + $sql = 'INSERT INTO `' . $this->tableName . '`(' . + $columns . ') VALUES(' . $values . ')'; + + $this->execute($sql, $params); + + $entity->setId((int) $this->api->getInsertId($this->tableName)); + return $entity; + } + + + + /** + * Updates an entry in the db from an entity + * @throws \InvalidArgumentException if entity has no id + * @param Entity $enttiy the entity that should be created + */ + public function update(Entity $entity){ + // entity needs an id + $id = $entity->getId(); + if($id === null){ + throw new \InvalidArgumentException( + 'Entity which should be updated has no id'); + } + + // get updated fields to save, fields have to be set using a setter to + // be saved + $properties = $entity->getUpdatedFields(); + // dont update the id field + unset($properties['id']); + + $columns = ''; + $params = array(); + + // build the fields + $i = 0; + foreach($properties as $property => $updated) { + + $column = $entity->propertyToColumn($property); + $getter = 'get' . ucfirst($property); + + $columns .= '`' . $column . '` = ?'; + + // only append colon if there are more entries + if($i < count($properties)-1){ + $columns .= ','; + } + + array_push($params, $entity->$getter()); + $i++; + } + + $sql = 'UPDATE `' . $this->tableName . '` SET ' . + $columns . ' WHERE `id` = ?'; + array_push($params, $id); + + $this->execute($sql, $params); + } + + + /** + * Runs an sql query + * @param string $sql the prepare string + * @param array $params the params which should replace the ? in the sql query + * @param int $limit the maximum number of rows + * @param int $offset from which row we want to start + * @return \PDOStatement the database query result + */ + protected function execute($sql, array $params=array(), $limit=null, $offset=null){ + $query = $this->api->prepareQuery($sql, $limit, $offset); + + $index = 1; // bindParam is 1 indexed + foreach($params as $param) { + + switch (gettype($param)) { + case 'integer': + $pdoConstant = \PDO::PARAM_INT; + break; + + case 'boolean': + $pdoConstant = \PDO::PARAM_BOOL; + break; + + default: + $pdoConstant = \PDO::PARAM_STR; + break; + } + + $query->bindValue($index, $param, $pdoConstant); + + $index++; + } + + return $query->execute(); + } + + + /** + * Returns an db result and throws exceptions when there are more or less + * results + * @see findEntity + * @param string $sql the sql query + * @param array $params the parameters of the sql query + * @param int $limit the maximum number of rows + * @param int $offset from which row we want to start + * @throws DoesNotExistException if the item does not exist + * @throws MultipleObjectsReturnedException if more than one item exist + * @return array the result as row + */ + protected function findOneQuery($sql, array $params=array(), $limit=null, $offset=null){ + $result = $this->execute($sql, $params, $limit, $offset); + $row = $result->fetchRow(); + + if($row === false || $row === null){ + throw new DoesNotExistException('No matching entry found'); + } + $row2 = $result->fetchRow(); + //MDB2 returns null, PDO and doctrine false when no row is available + if( ! ($row2 === false || $row2 === null )) { + throw new MultipleObjectsReturnedException('More than one result'); + } else { + return $row; + } + } + + + /** + * Creates an entity from a row. Automatically determines the entity class + * from the current mapper name (MyEntityMapper -> MyEntity) + * @param array $row the row which should be converted to an entity + * @return Entity the entity + */ + protected function mapRowToEntity($row) { + return call_user_func($this->entityClass .'::fromRow', $row); + } + + + /** + * Runs a sql query and returns an array of entities + * @param string $sql the prepare string + * @param array $params the params which should replace the ? in the sql query + * @param int $limit the maximum number of rows + * @param int $offset from which row we want to start + * @return array all fetched entities + */ + protected function findEntities($sql, array $params=array(), $limit=null, $offset=null) { + $result = $this->execute($sql, $params, $limit, $offset); + + $entities = array(); + + while($row = $result->fetchRow()){ + $entities[] = $this->mapRowToEntity($row); + } + + return $entities; + } + + + /** + * Returns an db result and throws exceptions when there are more or less + * results + * @param string $sql the sql query + * @param array $params the parameters of the sql query + * @param int $limit the maximum number of rows + * @param int $offset from which row we want to start + * @throws DoesNotExistException if the item does not exist + * @throws MultipleObjectsReturnedException if more than one item exist + * @return Entity the entity + */ + protected function findEntity($sql, array $params=array(), $limit=null, $offset=null){ + return $this->mapRowToEntity($this->findOneQuery($sql, $params, $limit, $offset)); + } + + +} diff --git a/db/multipleobjectsreturnedexception.php b/db/multipleobjectsreturnedexception.php new file mode 100644 index 000000000..0a8ca5686 --- /dev/null +++ b/db/multipleobjectsreturnedexception.php @@ -0,0 +1,42 @@ +. + * + */ + + +namespace OCA\News\Db; + + +/** + * This is returned or should be returned when a find request finds more than one + * row + */ +class MultipleObjectsReturnedException extends \Exception { + + /** + * Constructor + * @param string $msg the error message + */ + public function __construct($msg){ + parent::__construct($msg); + } + +} \ No newline at end of file diff --git a/db/postgres/itemmapper.php b/db/postgres/itemmapper.php index 0ccc9eeb4..be200ce93 100644 --- a/db/postgres/itemmapper.php +++ b/db/postgres/itemmapper.php @@ -24,11 +24,11 @@ namespace OCA\News\Db\Postgres; -use \OCA\AppFramework\Db\DoesNotExistException; -use \OCA\AppFramework\Db\MultipleObjectsReturnedException; -use \OCA\AppFramework\Db\Mapper; use \OCA\AppFramework\Core\API; +use \OCA\News\Db\DoesNotExistException; +use \OCA\News\Db\MultipleObjectsReturnedException; +use \OCA\News\Db\Mapper; use \OCA\News\Db\StatusFlag; -- cgit v1.2.3