Add AppFramework/Db base classes

This will allow apps to use Entities that do not have an id attribute.
They can have other unique columns forcing an id is not ideal in all
cases.

For this two base classes are introduced and the current Entity and
QBMapper are actually based on the base classes.

The Entity is really simple and just adds the id property.
The BaseMapper is a bit less clean. As we can't know the where clause
for insert/delete or update parts.

Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl>
This commit is contained in:
Roeland Jago Douma 2019-10-09 12:44:04 +02:00 committed by Daniel Kesselberg
parent 2ebf88a98c
commit c44511b099
No known key found for this signature in database
GPG Key ID: 36E3664E099D0614
6 changed files with 566 additions and 423 deletions

View File

@ -24,6 +24,8 @@ return array(
'OCP\\AppFramework\\App' => $baseDir . '/lib/public/AppFramework/App.php',
'OCP\\AppFramework\\AuthPublicShareController' => $baseDir . '/lib/public/AppFramework/AuthPublicShareController.php',
'OCP\\AppFramework\\Controller' => $baseDir . '/lib/public/AppFramework/Controller.php',
'OCP\\AppFramework\\Db\\AQBBaseMapper' => $baseDir . '/lib/public/AppFramework/Db/AQBBaseMapper.php',
'OCP\\AppFramework\\Db\\BaseEntity' => $baseDir . '/lib/public/AppFramework/Db/BaseEntity.php',
'OCP\\AppFramework\\Db\\DoesNotExistException' => $baseDir . '/lib/public/AppFramework/Db/DoesNotExistException.php',
'OCP\\AppFramework\\Db\\Entity' => $baseDir . '/lib/public/AppFramework/Db/Entity.php',
'OCP\\AppFramework\\Db\\IMapperException' => $baseDir . '/lib/public/AppFramework/Db/IMapperException.php',

View File

@ -53,6 +53,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
'OCP\\AppFramework\\App' => __DIR__ . '/../../..' . '/lib/public/AppFramework/App.php',
'OCP\\AppFramework\\AuthPublicShareController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/AuthPublicShareController.php',
'OCP\\AppFramework\\Controller' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Controller.php',
'OCP\\AppFramework\\Db\\AQBBaseMapper' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/AQBBaseMapper.php',
'OCP\\AppFramework\\Db\\BaseEntity' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/BaseEntity.php',
'OCP\\AppFramework\\Db\\DoesNotExistException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/DoesNotExistException.php',
'OCP\\AppFramework\\Db\\Entity' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/Entity.php',
'OCP\\AppFramework\\Db\\IMapperException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/IMapperException.php',

View File

@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCP\AppFramework\Db;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
* Simple parent class for inheriting your data access layer from. This class
* may be subject to change in the future
*
* @since 19.0.0
*/
abstract class AQBBaseMapper {
/** @var string */
protected $tableName;
/** @var string */
protected $entityClass;
/** @var IDBConnection */
protected $db;
/**
* @param IDBConnection $db Instance of the Db 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
* @since 14.0.0
*/
public function __construct(IDBConnection $db, string $tableName, string $entityClass = null) {
$this->db = $db;
$this->tableName = $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
* @since 14.0.0
*/
public function getTableName(): string {
return $this->tableName;
}
/**
* Deletes an entity from the table
* @param BaseEntity $entity the entity that should be deleted
* @return BaseEntity the deleted entity
* @since 14.0.0
*/
abstract public function delete(BaseEntity $entity): BaseEntity;
/**
* Creates a new entry in the db from an entity
* @param BaseEntity $entity the entity that should be created
* @return BaseEntity the saved entity with the set id
* @since 14.0.0
* @suppress SqlInjectionChecker
*/
public function insert(BaseEntity $entity): BaseEntity {
// get updated fields to save, fields have to be set using a setter to
// be saved
$properties = $entity->getUpdatedFields();
$qb = $this->db->getQueryBuilder();
$qb->insert($this->tableName);
// build the fields
foreach($properties as $property => $updated) {
$column = $entity->propertyToColumn($property);
$getter = 'get' . ucfirst($property);
$value = $entity->$getter();
$type = $this->getParameterTypeForProperty($entity, $property);
$qb->setValue($column, $qb->createNamedParameter($value, $type));
}
$qb->execute();
return $entity;
}
/**
* Tries to creates a new entry in the db from an entity and
* updates an existing entry if duplicate keys are detected
* by the database
*
* @param Entity $entity the entity that should be created/updated
* @return Entity the saved entity with the (new) id
* @throws \InvalidArgumentException if entity has no id
* @since 15.0.0
* @suppress SqlInjectionChecker
*/
public function insertOrUpdate(Entity $entity): Entity {
try {
return $this->insert($entity);
} catch (UniqueConstraintViolationException $ex) {
return $this->update($entity);
}
}
/**
* Updates an entry in the db from an entity
* @throws \InvalidArgumentException if entity has no id
* @param Entity $entity the entity that should be created
* @return Entity the saved entity with the set id
* @since 14.0.0
* @suppress SqlInjectionChecker
*/
abstract public function update(BaseEntity $entity): BaseEntity;
/**
* Returns the type parameter for the QueryBuilder for a specific property
* of the $entity
*
* @param BaseEntity $entity The entity to get the types from
* @param string $property The property of $entity to get the type for
* @return int
* @since 16.0.0
*/
protected function getParameterTypeForProperty(BaseEntity $entity, string $property): int {
$types = $entity->getFieldTypes();
if (!isset($types[$property])) {
return IQueryBuilder::PARAM_STR;
}
switch ($types[$property]) {
case 'int':
case 'integer':
return IQueryBuilder::PARAM_INT;
case 'string':
return IQueryBuilder::PARAM_STR;
case 'bool':
case 'boolean':
return IQueryBuilder::PARAM_BOOL;
}
return IQueryBuilder::PARAM_STR;
}
/**
* Returns an db result and throws exceptions when there are more or less
* results
*
* @param IQueryBuilder $query
* @return array the result as row
* @throws MultipleObjectsReturnedException if more than one item exist
* @throws DoesNotExistException if the item does not exist
* @see findEntity
*
* @since 14.0.0
*/
protected function findOneQuery(IQueryBuilder $query): array {
$cursor = $query->execute();
$row = $cursor->fetch();
if ($row === false) {
$cursor->closeCursor();
$msg = $this->buildDebugMessage(
'Did expect one result but found none when executing', $query
);
throw new DoesNotExistException($msg);
}
$row2 = $cursor->fetch();
$cursor->closeCursor();
if ($row2 !== false) {
$msg = $this->buildDebugMessage(
'Did not expect more than one result when executing', $query
);
throw new MultipleObjectsReturnedException($msg);
}
return $row;
}
/**
* @param string $msg
* @param IQueryBuilder $sql
* @return string
* @since 14.0.0
*/
private function buildDebugMessage(string $msg, IQueryBuilder $sql): string {
return $msg .
': query "' . $sql->getSQL() . '"; ';
}
/**
* 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 BaseEntity the entity
* @since 14.0.0
*/
protected function mapRowToEntity(array $row): BaseEntity {
return \call_user_func($this->entityClass . '::fromRow', $row);
}
/**
* Runs a sql query and returns an array of entities
*
* @param IQueryBuilder $query
* @return BaseEntity[] all fetched entities
* @since 14.0.0
*/
protected function findEntities(IQueryBuilder $query): array {
$cursor = $query->execute();
$entities = [];
while ($row = $cursor->fetch()) {
$entities[] = $this->mapRowToEntity($row);
}
$cursor->closeCursor();
return $entities;
}
/**
* Returns an db result and throws exceptions when there are more or less
* results
*
* @param IQueryBuilder $query
* @return BaseEntity the entity
* @throws MultipleObjectsReturnedException if more than one item exist
* @throws DoesNotExistException if the item does not exist
* @since 14.0.0
*/
protected function findEntity(IQueryBuilder $query): BaseEntity {
return $this->mapRowToEntity($this->findOneQuery($query));
}
}

View File

@ -0,0 +1,275 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
*
* @author Roeland Jago Douma <roeland@famdouma.nl>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCP\AppFramework\Db;
/**
* Base entity class that does not require a id column
*
* @since 19.0.0
*/
class BaseEntity {
private $_updatedFields = [];
private $_fieldTypes = [];
/**
* 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 BaseEntity
* @since 7.0.0
*/
public static function fromParams(array $params) {
$instance = new static();
foreach($params as $key => $value) {
$setter = 'set' . ucfirst($key);
$value = $instance->convertToType($key, $value);
$instance->$setter($value);
}
return $instance;
}
/**
* Maps the keys of the row array to the attributes
* @param array $row the row to map onto the entity
* @since 7.0.0
* @return BaseEntity
*/
public static function fromRow(array $row){
$instance = new static();
foreach($row as $key => $value){
$key = $instance->columnToProperty($key);
$setter = 'set' . ucfirst($key);
$value = $instance->convertToType($key, $value);
$instance->$setter($value);
}
$instance->resetUpdatedFields();
return $instance;
}
/**
* @return array with attribute and type
* @since 7.0.0
*/
public function getFieldTypes(): array {
return $this->_fieldTypes;
}
/**
* Marks the entity as clean needed for setting the id after the insertion
* @since 7.0.0
*/
public function resetUpdatedFields(): void {
$this->_updatedFields = [];
}
/**
* Generic setter for properties
* @since 7.0.0
*/
protected function setter($name, $args) {
// setters should only work for existing attributes
if(property_exists($this, $name)){
if($this->$name === $args[0]) {
return;
}
$this->markFieldUpdated($name);
// if type definition exists, cast to correct type
$value = $this->convertToType($name, $args[0]);
$this->$name = $value;
} else {
throw new \BadFunctionCallException($name .
' is not a valid attribute');
}
}
/**
* Generic getter for properties
* @since 7.0.0
*/
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
* @since 7.0.0
*/
public function __call($methodName, $args){
if(strpos($methodName, 'set') === 0){
$this->setter(lcfirst(substr($methodName, 3)), $args);
} elseif(strpos($methodName, 'get') === 0) {
return $this->getter(lcfirst(substr($methodName, 3)));
} elseif ($this->isGetterForBoolProperty($methodName)) {
return $this->getter(lcfirst(substr($methodName, 2)));
} else {
throw new \BadFunctionCallException($methodName .
' does not exist');
}
}
/**
* @param string $methodName
* @return bool
* @since 18.0.0
*/
protected function isGetterForBoolProperty(string $methodName): bool {
if (strpos($methodName, 'is') === 0) {
$fieldName = lcfirst(substr($methodName, 2));
return isset($this->_fieldTypes[$fieldName]) && strpos($this->_fieldTypes[$fieldName], 'bool') === 0;
}
return false;
}
/**
* Mark am attribute as updated
* @param string $attribute the name of the attribute
* @since 7.0.0
*/
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
* @since 7.0.0
*/
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
* @since 7.0.0
*/
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
* @since 7.0.0
*/
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()
* @since 7.0.0
*/
protected function addType(string $fieldName, string $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
* @since 7.0.0
*/
public function slugify(string $attributeName): string {
// 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');
}
}
/**
* @since 19.0.0
*/
protected function convertToType(string $name, $value) {
if ($value !== null && array_key_exists($name, $this->getFieldTypes())) {
settype($value, $this->getFieldTypes()[$name]);
}
return $value;
}
}

View File

@ -34,238 +34,18 @@ use function substr;
* @method void setId(integer $id)
* @since 7.0.0
*/
abstract class Entity {
abstract class Entity extends BaseEntity {
/** @var int */
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
* @since 7.0.0
*/
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
* @since 7.0.0
*/
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 array with attribute and type
* @since 7.0.0
*/
public function getFieldTypes() {
return $this->_fieldTypes;
public function getFieldTypes(): array {
$types = parent::getFieldTypes();
$types['id'] = 'integer';
return $types;
}
/**
* Marks the entity as clean needed for setting the id after the insertion
* @since 7.0.0
*/
public function resetUpdatedFields(){
$this->_updatedFields = array();
}
/**
* Generic setter for properties
* @since 7.0.0
*/
protected function setter($name, $args) {
// setters should only work for existing attributes
if(property_exists($this, $name)){
if($this->$name === $args[0]) {
return;
}
$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');
}
}
/**
* Generic getter for properties
* @since 7.0.0
*/
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
* @since 7.0.0
*/
public function __call($methodName, $args) {
if (strpos($methodName, 'set') === 0) {
$this->setter(lcfirst(substr($methodName, 3)), $args);
} elseif (strpos($methodName, 'get') === 0) {
return $this->getter(lcfirst(substr($methodName, 3)));
} elseif ($this->isGetterForBoolProperty($methodName)) {
return $this->getter(lcfirst(substr($methodName, 2)));
} else {
throw new \BadFunctionCallException($methodName .
' does not exist');
}
}
/**
* @param string $methodName
* @return bool
* @since 18.0.0
*/
protected function isGetterForBoolProperty(string $methodName): bool {
if (strpos($methodName, 'is') === 0) {
$fieldName = lcfirst(substr($methodName, 2));
return isset($this->_fieldTypes[$fieldName]) && strpos($this->_fieldTypes[$fieldName], 'bool') === 0;
}
return false;
}
/**
* Mark am attribute as updated
* @param string $attribute the name of the attribute
* @since 7.0.0
*/
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
* @since 7.0.0
*/
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
* @since 7.0.0
*/
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
* @since 7.0.0
*/
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()
* @since 7.0.0
*/
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
* @since 7.0.0
*/
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');
}
}
}

View File

@ -31,7 +31,6 @@ namespace OCP\AppFramework\Db;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
* Simple parent class for inheriting your data access layer from. This class
@ -39,54 +38,15 @@ use OCP\IDBConnection;
*
* @since 14.0.0
*/
abstract class QBMapper {
/** @var string */
protected $tableName;
/** @var string */
protected $entityClass;
/** @var IDBConnection */
protected $db;
/**
* @param IDBConnection $db Instance of the Db 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
* @since 14.0.0
*/
public function __construct(IDBConnection $db, string $tableName, string $entityClass=null){
$this->db = $db;
$this->tableName = $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
* @since 14.0.0
*/
public function getTableName(): string {
return $this->tableName;
}
abstract class QBMapper extends AQBBaseMapper {
/**
* Deletes an entity from the table
* @param Entity $entity the entity that should be deleted
* @return Entity the deleted entity
* @param BaseEntity $entity the entity that should be deleted
* @return BaseEntity the deleted entity
* @since 14.0.0
*/
public function delete(Entity $entity): Entity {
public function delete(BaseEntity $entity): BaseEntity {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->tableName)
@ -100,12 +60,12 @@ abstract class QBMapper {
/**
* Creates a new entry in the db from an entity
* @param Entity $entity the entity that should be created
* @return Entity the saved entity with the set id
* @param BaseEntity $entity the entity that should be created
* @return BaseEntity the saved entity with the set id
* @since 14.0.0
* @suppress SqlInjectionChecker
*/
public function insert(Entity $entity): Entity {
public function insert(BaseEntity $entity): BaseEntity {
// get updated fields to save, fields have to be set using a setter to
// be saved
$properties = $entity->getUpdatedFields();
@ -132,34 +92,15 @@ abstract class QBMapper {
return $entity;
}
/**
* Tries to creates a new entry in the db from an entity and
* updates an existing entry if duplicate keys are detected
* by the database
*
* @param Entity $entity the entity that should be created/updated
* @return Entity the saved entity with the (new) id
* @throws \InvalidArgumentException if entity has no id
* @since 15.0.0
* @suppress SqlInjectionChecker
*/
public function insertOrUpdate(Entity $entity): Entity {
try {
return $this->insert($entity);
} catch (UniqueConstraintViolationException $ex) {
return $this->update($entity);
}
}
/**
* Updates an entry in the db from an entity
* @throws \InvalidArgumentException if entity has no id
* @param Entity $entity the entity that should be created
* @return Entity the saved entity with the set id
* @param BaseEntity $entity the entity that should be created
* @return BaseEntity the saved entity with the set id
* @since 14.0.0
* @suppress SqlInjectionChecker
*/
public function update(Entity $entity): Entity {
public function update(BaseEntity $entity): BaseEntity {
// if entity wasn't changed it makes no sense to run a db query
$properties = $entity->getUpdatedFields();
if(\count($properties) === 0) {
@ -198,132 +139,4 @@ abstract class QBMapper {
return $entity;
}
/**
* Returns the type parameter for the QueryBuilder for a specific property
* of the $entity
*
* @param Entity $entity The entity to get the types from
* @param string $property The property of $entity to get the type for
* @return int
* @since 16.0.0
*/
protected function getParameterTypeForProperty(Entity $entity, string $property): int {
$types = $entity->getFieldTypes();
if(!isset($types[ $property ])) {
return IQueryBuilder::PARAM_STR;
}
switch($types[ $property ]) {
case 'int':
case 'integer':
return IQueryBuilder::PARAM_INT;
case 'string':
return IQueryBuilder::PARAM_STR;
case 'bool':
case 'boolean':
return IQueryBuilder::PARAM_BOOL;
}
return IQueryBuilder::PARAM_STR;
}
/**
* Returns an db result and throws exceptions when there are more or less
* results
*
* @see findEntity
*
* @param IQueryBuilder $query
* @throws DoesNotExistException if the item does not exist
* @throws MultipleObjectsReturnedException if more than one item exist
* @return array the result as row
* @since 14.0.0
*/
protected function findOneQuery(IQueryBuilder $query): array {
$cursor = $query->execute();
$row = $cursor->fetch();
if($row === false) {
$cursor->closeCursor();
$msg = $this->buildDebugMessage(
'Did expect one result but found none when executing', $query
);
throw new DoesNotExistException($msg);
}
$row2 = $cursor->fetch();
$cursor->closeCursor();
if($row2 !== false ) {
$msg = $this->buildDebugMessage(
'Did not expect more than one result when executing', $query
);
throw new MultipleObjectsReturnedException($msg);
}
return $row;
}
/**
* @param string $msg
* @param IQueryBuilder $sql
* @return string
* @since 14.0.0
*/
private function buildDebugMessage(string $msg, IQueryBuilder $sql): string {
return $msg .
': query "' . $sql->getSQL() . '"; ';
}
/**
* 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
* @since 14.0.0
*/
protected function mapRowToEntity(array $row): Entity {
return \call_user_func($this->entityClass .'::fromRow', $row);
}
/**
* Runs a sql query and returns an array of entities
*
* @param IQueryBuilder $query
* @return Entity[] all fetched entities
* @since 14.0.0
*/
protected function findEntities(IQueryBuilder $query): array {
$cursor = $query->execute();
$entities = [];
while($row = $cursor->fetch()){
$entities[] = $this->mapRowToEntity($row);
}
$cursor->closeCursor();
return $entities;
}
/**
* Returns an db result and throws exceptions when there are more or less
* results
*
* @param IQueryBuilder $query
* @throws DoesNotExistException if the item does not exist
* @throws MultipleObjectsReturnedException if more than one item exist
* @return Entity the entity
* @since 14.0.0
*/
protected function findEntity(IQueryBuilder $query): Entity {
return $this->mapRowToEntity($this->findOneQuery($query));
}
}