*/ 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 class-string|null $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 Entity $entity the entity that should be deleted * @psalm-param T $entity the entity that should be deleted * @return Entity the deleted entity * @psalm-return T the deleted entity * @throws Exception * @since 14.0.0 */ public function delete(Entity $entity): Entity { $qb = $this->db->getQueryBuilder(); $idType = $this->getParameterTypeForProperty($entity, 'id'); $qb->delete($this->tableName) ->where( $qb->expr()->eq('id', $qb->createNamedParameter($entity->getId(), $idType)) ); $qb->executeStatement(); return $entity; } /** * Creates a new entry in the db from an entity * * @param Entity $entity the entity that should be created * @psalm-param T $entity the entity that should be created * @return Entity the saved entity with the set id * @psalm-return T the saved entity with the set id * @throws Exception * @since 14.0.0 */ public function insert(Entity $entity): Entity { // 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->executeStatement(); if ($entity->id === null) { // When autoincrement is used id is always an int $entity->setId($qb->getLastInsertId()); } 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 * @psalm-param T $entity the entity that should be created/updated * @return Entity the saved entity with the (new) id * @psalm-return T the saved entity with the (new) id * @throws Exception * @throws \InvalidArgumentException if entity has no id * @since 15.0.0 */ public function insertOrUpdate(Entity $entity): Entity { try { return $this->insert($entity); } catch (Exception $ex) { if ($ex->getReason() === Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION) { return $this->update($entity); } throw $ex; } } /** * Updates an entry in the db from an entity * * @param Entity $entity the entity that should be created * @psalm-param T $entity the entity that should be created * @return Entity the saved entity with the set id * @psalm-return T the saved entity with the set id * @throws Exception * @throws \InvalidArgumentException if entity has no id * @since 14.0.0 */ public function update(Entity $entity): Entity { // if entity wasn't changed it makes no sense to run a db query $properties = $entity->getUpdatedFields(); if (\count($properties) === 0) { return $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 // do not update the id field unset($properties['id']); $qb = $this->db->getQueryBuilder(); $qb->update($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->set($column, $qb->createNamedParameter($value, $type)); } $idType = $this->getParameterTypeForProperty($entity, 'id'); $qb->where( $qb->expr()->eq('id', $qb->createNamedParameter($id, $idType)) ); $qb->executeStatement(); 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 * @psalm-param T $entity * @param string $property The property of $entity to get the type for * @return int|string * @since 16.0.0 */ protected function getParameterTypeForProperty(Entity $entity, string $property) { $types = $entity->getFieldTypes(); if (!isset($types[ $property ])) { return IQueryBuilder::PARAM_STR; } switch ($types[ $property ]) { case 'int': case Types::INTEGER: case Types::SMALLINT: return IQueryBuilder::PARAM_INT; case Types::STRING: return IQueryBuilder::PARAM_STR; case 'bool': case Types::BOOLEAN: return IQueryBuilder::PARAM_BOOL; case Types::BLOB: return IQueryBuilder::PARAM_LOB; case Types::DATE: return IQueryBuilder::PARAM_DATETIME_MUTABLE; case Types::DATETIME: return IQueryBuilder::PARAM_DATETIME_MUTABLE; case Types::DATETIME_TZ: return IQueryBuilder::PARAM_DATETIME_TZ_MUTABLE; case Types::DATE_IMMUTABLE: return IQueryBuilder::PARAM_DATE_IMMUTABLE; case Types::DATETIME_IMMUTABLE: return IQueryBuilder::PARAM_DATETIME_IMMUTABLE; case Types::DATETIME_TZ_IMMUTABLE: return IQueryBuilder::PARAM_DATETIME_TZ_IMMUTABLE; case Types::TIME: return IQueryBuilder::PARAM_TIME_MUTABLE; case Types::TIME_IMMUTABLE: return IQueryBuilder::PARAM_TIME_IMMUTABLE; case Types::JSON: return IQueryBuilder::PARAM_JSON; } 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 Exception * @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 { $result = $query->executeQuery(); $row = $result->fetch(); if ($row === false) { $result->closeCursor(); $msg = $this->buildDebugMessage( 'Did expect one result but found none when executing', $query ); throw new DoesNotExistException($msg); } $row2 = $result->fetch(); $result->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 * @psalm-return T the entity * @since 14.0.0 */ protected function mapRowToEntity(array $row): Entity { unset($row['DOCTRINE_ROWNUM']); // remove doctrine/dbal helper column return \call_user_func($this->entityClass . '::fromRow', $row); } /** * Runs a sql query and returns an array of entities * * @param IQueryBuilder $query * @return list all fetched entities * @psalm-return list all fetched entities * @throws Exception * @since 14.0.0 */ protected function findEntities(IQueryBuilder $query): array { $result = $query->executeQuery(); try { $entities = []; while ($row = $result->fetch()) { $entities[] = $this->mapRowToEntity($row); } return $entities; } finally { $result->closeCursor(); } } /** * Runs a sql query and yields each resulting entity to obtain database entries in a memory-efficient way * * @param IQueryBuilder $query * @return Generator Generator of fetched entities * @psalm-return Generator Generator of fetched entities * @throws Exception * @since 30.0.0 */ protected function yieldEntities(IQueryBuilder $query): Generator { $result = $query->executeQuery(); try { while ($row = $result->fetch()) { yield $this->mapRowToEntity($row); } } finally { $result->closeCursor(); } } /** * Returns an db result and throws exceptions when there are more or less * results * * @param IQueryBuilder $query * @return Entity the entity * @psalm-return T the entity * @throws Exception * @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): Entity { return $this->mapRowToEntity($this->findOneQuery($query)); } }