Skip to content

Commit 001c624

Browse files
committed
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 <[email protected]>
1 parent 2be1a6a commit 001c624

6 files changed

Lines changed: 566 additions & 423 deletions

File tree

lib/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
'OCP\\AppFramework\\App' => $baseDir . '/lib/public/AppFramework/App.php',
2525
'OCP\\AppFramework\\AuthPublicShareController' => $baseDir . '/lib/public/AppFramework/AuthPublicShareController.php',
2626
'OCP\\AppFramework\\Controller' => $baseDir . '/lib/public/AppFramework/Controller.php',
27+
'OCP\\AppFramework\\Db\\AQBBaseMapper' => $baseDir . '/lib/public/AppFramework/Db/AQBBaseMapper.php',
28+
'OCP\\AppFramework\\Db\\BaseEntity' => $baseDir . '/lib/public/AppFramework/Db/BaseEntity.php',
2729
'OCP\\AppFramework\\Db\\DoesNotExistException' => $baseDir . '/lib/public/AppFramework/Db/DoesNotExistException.php',
2830
'OCP\\AppFramework\\Db\\Entity' => $baseDir . '/lib/public/AppFramework/Db/Entity.php',
2931
'OCP\\AppFramework\\Db\\IMapperException' => $baseDir . '/lib/public/AppFramework/Db/IMapperException.php',

lib/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c
5353
'OCP\\AppFramework\\App' => __DIR__ . '/../../..' . '/lib/public/AppFramework/App.php',
5454
'OCP\\AppFramework\\AuthPublicShareController' => __DIR__ . '/../../..' . '/lib/public/AppFramework/AuthPublicShareController.php',
5555
'OCP\\AppFramework\\Controller' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Controller.php',
56+
'OCP\\AppFramework\\Db\\AQBBaseMapper' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/AQBBaseMapper.php',
57+
'OCP\\AppFramework\\Db\\BaseEntity' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/BaseEntity.php',
5658
'OCP\\AppFramework\\Db\\DoesNotExistException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/DoesNotExistException.php',
5759
'OCP\\AppFramework\\Db\\Entity' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/Entity.php',
5860
'OCP\\AppFramework\\Db\\IMapperException' => __DIR__ . '/../../..' . '/lib/public/AppFramework/Db/IMapperException.php',
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
<?php
2+
declare(strict_types=1);
3+
/**
4+
* @copyright Copyright (c) 2019, Roeland Jago Douma <[email protected]>
5+
*
6+
* @author Roeland Jago Douma <[email protected]>
7+
*
8+
* @license GNU AGPL version 3 or any later version
9+
*
10+
* This program is free software: you can redistribute it and/or modify
11+
* it under the terms of the GNU Affero General Public License as
12+
* published by the Free Software Foundation, either version 3 of the
13+
* License, or (at your option) any later version.
14+
*
15+
* This program is distributed in the hope that it will be useful,
16+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
* GNU Affero General Public License for more details.
19+
*
20+
* You should have received a copy of the GNU Affero General Public License
21+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
22+
*
23+
*/
24+
25+
26+
namespace OCP\AppFramework\Db;
27+
28+
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
29+
use OCP\DB\QueryBuilder\IQueryBuilder;
30+
use OCP\IDBConnection;
31+
32+
/**
33+
* Simple parent class for inheriting your data access layer from. This class
34+
* may be subject to change in the future
35+
*
36+
* @since 19.0.0
37+
*/
38+
abstract class AQBBaseMapper {
39+
40+
/** @var string */
41+
protected $tableName;
42+
43+
/** @var string */
44+
protected $entityClass;
45+
46+
/** @var IDBConnection */
47+
protected $db;
48+
49+
/**
50+
* @param IDBConnection $db Instance of the Db abstraction layer
51+
* @param string $tableName the name of the table. set this to allow entity
52+
* @param string $entityClass the name of the entity that the sql should be
53+
* mapped to queries without using sql
54+
* @since 14.0.0
55+
*/
56+
public function __construct(IDBConnection $db, string $tableName, string $entityClass = null) {
57+
$this->db = $db;
58+
$this->tableName = $tableName;
59+
60+
// if not given set the entity name to the class without the mapper part
61+
// cache it here for later use since reflection is slow
62+
if ($entityClass === null) {
63+
$this->entityClass = str_replace('Mapper', '', \get_class($this));
64+
} else {
65+
$this->entityClass = $entityClass;
66+
}
67+
}
68+
69+
70+
/**
71+
* @return string the table name
72+
* @since 14.0.0
73+
*/
74+
public function getTableName(): string {
75+
return $this->tableName;
76+
}
77+
78+
/**
79+
* Deletes an entity from the table
80+
* @param BaseEntity $entity the entity that should be deleted
81+
* @return BaseEntity the deleted entity
82+
* @since 14.0.0
83+
*/
84+
abstract public function delete(BaseEntity $entity): BaseEntity;
85+
86+
/**
87+
* Creates a new entry in the db from an entity
88+
* @param BaseEntity $entity the entity that should be created
89+
* @return BaseEntity the saved entity with the set id
90+
* @since 14.0.0
91+
* @suppress SqlInjectionChecker
92+
*/
93+
public function insert(BaseEntity $entity): BaseEntity {
94+
// get updated fields to save, fields have to be set using a setter to
95+
// be saved
96+
$properties = $entity->getUpdatedFields();
97+
98+
$qb = $this->db->getQueryBuilder();
99+
$qb->insert($this->tableName);
100+
101+
// build the fields
102+
foreach($properties as $property => $updated) {
103+
$column = $entity->propertyToColumn($property);
104+
$getter = 'get' . ucfirst($property);
105+
$value = $entity->$getter();
106+
107+
$type = $this->getParameterTypeForProperty($entity, $property);
108+
$qb->setValue($column, $qb->createNamedParameter($value, $type));
109+
}
110+
$qb->execute();
111+
112+
return $entity;
113+
}
114+
115+
/**
116+
* Tries to creates a new entry in the db from an entity and
117+
* updates an existing entry if duplicate keys are detected
118+
* by the database
119+
*
120+
* @param Entity $entity the entity that should be created/updated
121+
* @return Entity the saved entity with the (new) id
122+
* @throws \InvalidArgumentException if entity has no id
123+
* @since 15.0.0
124+
* @suppress SqlInjectionChecker
125+
*/
126+
public function insertOrUpdate(Entity $entity): Entity {
127+
try {
128+
return $this->insert($entity);
129+
} catch (UniqueConstraintViolationException $ex) {
130+
return $this->update($entity);
131+
}
132+
}
133+
134+
/**
135+
* Updates an entry in the db from an entity
136+
* @throws \InvalidArgumentException if entity has no id
137+
* @param Entity $entity the entity that should be created
138+
* @return Entity the saved entity with the set id
139+
* @since 14.0.0
140+
* @suppress SqlInjectionChecker
141+
*/
142+
abstract public function update(BaseEntity $entity): BaseEntity;
143+
144+
/**
145+
* Returns the type parameter for the QueryBuilder for a specific property
146+
* of the $entity
147+
*
148+
* @param BaseEntity $entity The entity to get the types from
149+
* @param string $property The property of $entity to get the type for
150+
* @return int
151+
* @since 16.0.0
152+
*/
153+
protected function getParameterTypeForProperty(BaseEntity $entity, string $property): int {
154+
$types = $entity->getFieldTypes();
155+
156+
if (!isset($types[$property])) {
157+
return IQueryBuilder::PARAM_STR;
158+
}
159+
160+
switch ($types[$property]) {
161+
case 'int':
162+
case 'integer':
163+
return IQueryBuilder::PARAM_INT;
164+
case 'string':
165+
return IQueryBuilder::PARAM_STR;
166+
case 'bool':
167+
case 'boolean':
168+
return IQueryBuilder::PARAM_BOOL;
169+
}
170+
171+
return IQueryBuilder::PARAM_STR;
172+
}
173+
174+
/**
175+
* Returns an db result and throws exceptions when there are more or less
176+
* results
177+
*
178+
* @param IQueryBuilder $query
179+
* @return array the result as row
180+
* @throws MultipleObjectsReturnedException if more than one item exist
181+
* @throws DoesNotExistException if the item does not exist
182+
* @see findEntity
183+
*
184+
* @since 14.0.0
185+
*/
186+
protected function findOneQuery(IQueryBuilder $query): array {
187+
$cursor = $query->execute();
188+
189+
$row = $cursor->fetch();
190+
if ($row === false) {
191+
$cursor->closeCursor();
192+
$msg = $this->buildDebugMessage(
193+
'Did expect one result but found none when executing', $query
194+
);
195+
throw new DoesNotExistException($msg);
196+
}
197+
198+
$row2 = $cursor->fetch();
199+
$cursor->closeCursor();
200+
if ($row2 !== false) {
201+
$msg = $this->buildDebugMessage(
202+
'Did not expect more than one result when executing', $query
203+
);
204+
throw new MultipleObjectsReturnedException($msg);
205+
}
206+
207+
return $row;
208+
}
209+
210+
/**
211+
* @param string $msg
212+
* @param IQueryBuilder $sql
213+
* @return string
214+
* @since 14.0.0
215+
*/
216+
private function buildDebugMessage(string $msg, IQueryBuilder $sql): string {
217+
return $msg .
218+
': query "' . $sql->getSQL() . '"; ';
219+
}
220+
221+
222+
/**
223+
* Creates an entity from a row. Automatically determines the entity class
224+
* from the current mapper name (MyEntityMapper -> MyEntity)
225+
*
226+
* @param array $row the row which should be converted to an entity
227+
* @return BaseEntity the entity
228+
* @since 14.0.0
229+
*/
230+
protected function mapRowToEntity(array $row): BaseEntity {
231+
return \call_user_func($this->entityClass . '::fromRow', $row);
232+
}
233+
234+
235+
/**
236+
* Runs a sql query and returns an array of entities
237+
*
238+
* @param IQueryBuilder $query
239+
* @return BaseEntity[] all fetched entities
240+
* @since 14.0.0
241+
*/
242+
protected function findEntities(IQueryBuilder $query): array {
243+
$cursor = $query->execute();
244+
245+
$entities = [];
246+
247+
while ($row = $cursor->fetch()) {
248+
$entities[] = $this->mapRowToEntity($row);
249+
}
250+
251+
$cursor->closeCursor();
252+
253+
return $entities;
254+
}
255+
256+
257+
/**
258+
* Returns an db result and throws exceptions when there are more or less
259+
* results
260+
*
261+
* @param IQueryBuilder $query
262+
* @return BaseEntity the entity
263+
* @throws MultipleObjectsReturnedException if more than one item exist
264+
* @throws DoesNotExistException if the item does not exist
265+
* @since 14.0.0
266+
*/
267+
protected function findEntity(IQueryBuilder $query): BaseEntity {
268+
return $this->mapRowToEntity($this->findOneQuery($query));
269+
}
270+
271+
}

0 commit comments

Comments
 (0)