From 14cd485ce2c541ae46827a1eebc676b3db711a43 Mon Sep 17 00:00:00 2001 From: Carl Schwan Date: Tue, 4 Nov 2025 16:01:41 +0100 Subject: [PATCH] feat(entity): Attributes for Entity Signed-off-by: Carl Schwan --- .../lib/Db/BackupCode.php | 19 ++-- lib/private/Tagging/Tag.php | 65 ++++------- .../AppFramework/Db/Attribute/Column.php | 28 +++++ .../AppFramework/Db/Attribute/Table.php | 26 +++++ lib/public/AppFramework/Db/Entity.php | 51 ++++++++- lib/public/AppFramework/Db/QBMapper.php | 107 +++++++++++++++--- 6 files changed, 226 insertions(+), 70 deletions(-) create mode 100644 lib/public/AppFramework/Db/Attribute/Column.php create mode 100644 lib/public/AppFramework/Db/Attribute/Table.php diff --git a/apps/twofactor_backupcodes/lib/Db/BackupCode.php b/apps/twofactor_backupcodes/lib/Db/BackupCode.php index 252b9b77faa7c..f47981cda3f9c 100644 --- a/apps/twofactor_backupcodes/lib/Db/BackupCode.php +++ b/apps/twofactor_backupcodes/lib/Db/BackupCode.php @@ -8,9 +8,14 @@ */ namespace OCA\TwoFactorBackupCodes\Db; +use OCP\AppFramework\Db\Attribute\Column; +use OCP\AppFramework\Db\Attribute\Table; use OCP\AppFramework\Db\Entity; +use OCP\DB\Types; /** + * @method string getId() + * @method void setId(string $id) * @method string getUserId() * @method void setUserId(string $userId) * @method string getCode() @@ -18,14 +23,14 @@ * @method int getUsed() * @method void setUsed(int $code) */ +#[Table(name: 'twofactor_backupcodes', useSnowflakeId: true)] class BackupCode extends Entity { + #[Column(name: 'user_id', type: Types::STRING, length: 64, nullable: false)] + protected ?string $userId = null; - /** @var string */ - protected $userId; + #[Column(name: 'code', type: Types::STRING, length: 128, nullable: false)] + protected ?string $code = null; - /** @var string */ - protected $code; - - /** @var int */ - protected $used; + #[Column(name: 'used', type: Types::SMALLINT, nullable: false)] + protected ?int $used = null; } diff --git a/lib/private/Tagging/Tag.php b/lib/private/Tagging/Tag.php index 55a3a410281cb..89dd66a23cfbb 100644 --- a/lib/private/Tagging/Tag.php +++ b/lib/private/Tagging/Tag.php @@ -7,11 +7,16 @@ */ namespace OC\Tagging; +use OCP\AppFramework\Db\Attribute\Column; +use OCP\AppFramework\Db\Attribute\Table; use OCP\AppFramework\Db\Entity; +use OCP\DB\Types; /** * Class to represent a tag. * + * @method string getId() + * @method void setId(string $id) * @method string getOwner() * @method void setOwner(string $owner) * @method string getType() @@ -19,59 +24,29 @@ * @method string getName() * @method void setName(string $name) */ +#[Table(name: 'vcategory', useSnowflakeId: true)] class Tag extends Entity { - protected $owner; - protected $type; - protected $name; + #[Column(name: 'uid', type: Types::STRING, length: 64, nullable: false)] + protected ?string $owner = null; + + #[Column(name: 'type', type: Types::STRING, length: 64, nullable: false)] + protected ?string $type = null; + + #[Column(name: 'category', type: Types::STRING, length: 255, nullable: false)] + protected ?string $name = null; /** * Constructor. * - * @param string $owner The tag's owner - * @param string $type The type of item this tag is used for - * @param string $name The tag's name + * @param ?string $owner The tag's owner + * @param ?string $type The type of item this tag is used for + * @param ?string $name The tag's name */ - public function __construct($owner = null, $type = null, $name = null) { + public function __construct(?string $owner = null, ?string $type = null, ?string $name = null) { + parent::__construct(); + $this->setOwner($owner); $this->setType($type); $this->setName($name); } - - /** - * Transform a database columnname to a property - * - * @param string $columnName the name of the column - * @return string the property name - * @todo migrate existing database columns to the correct names - * to be able to drop this direct mapping - */ - public function columnToProperty(string $columnName): string { - if ($columnName === 'category') { - return 'name'; - } - - if ($columnName === 'uid') { - return 'owner'; - } - - return parent::columnToProperty($columnName); - } - - /** - * Transform a property to a database column name - * - * @param string $property the name of the property - * @return string the column name - */ - public function propertyToColumn(string $property): string { - if ($property === 'name') { - return 'category'; - } - - if ($property === 'owner') { - return 'uid'; - } - - return parent::propertyToColumn($property); - } } diff --git a/lib/public/AppFramework/Db/Attribute/Column.php b/lib/public/AppFramework/Db/Attribute/Column.php new file mode 100644 index 0000000000000..61223e1990e75 --- /dev/null +++ b/lib/public/AppFramework/Db/Attribute/Column.php @@ -0,0 +1,28 @@ + */ private array $_updatedFields = []; + /** @var array */ - private array $_fieldTypes = ['id' => 'integer']; + private array $_fieldTypes = ['id' => Types::INTEGER]; + + /** @var array */ + private array $_mappingColumnToProperty = []; + + /** @var array */ + private array $_mappingPropertyToColumn = []; + + public function __construct() { + $reflection = new \ReflectionObject($this); + + foreach ($reflection->getProperties() as $property) { + $columnAttributes = $property->getAttributes(Column::class); + if (count($columnAttributes) > 0) { + /** @var Column $columnAttribute */ + $columnAttribute = $columnAttributes[0]; + $this->_fieldTypes[$property->name] = $columnAttribute->type; + $this->_mappingColumnToProperty[$columnAttribute->name] = $property->name; + $this->_mappingPropertyToColumn[$property->name] = $columnAttribute->name; + } + } + + $tableAttributes =$reflection->getAttributes(Table::class); + if (count($tableAttributes) > 0) { + /** @var Table $tableAttribute */ + $tableAttribute = $tableAttributes[0]; + if ($tableAttribute->useSnowflakeId) { + $this->_fieldTypes['id'] = Types::STRING; + } + } + } /** * Simple alternative constructor for building entities from a request @@ -101,6 +135,7 @@ protected function setter(string $name, array $args): void { // if type definition exists, cast to correct type if ($args[0] !== null && array_key_exists($name, $this->_fieldTypes)) { $type = $this->_fieldTypes[$name]; + if ($type === Types::BLOB) { // (B)LOB is treated as string when we read from the DB if (is_resource($args[0])) { @@ -212,11 +247,16 @@ protected function markFieldUpdated(string $attribute): void { * @param string $columnName the name of the column * @return string the property name * @since 7.0.0 + * @deprecated Use Column attribute to map a property to a column */ public function columnToProperty(string $columnName) { $parts = explode('_', $columnName); $property = ''; + if (isset($this->_mappingColumnToProperty[$columnName])) { + return $this->_mappingColumnToProperty[$columnName]; + } + foreach ($parts as $part) { if ($property === '') { $property = $part; @@ -235,10 +275,15 @@ public function columnToProperty(string $columnName) { * @param string $property the name of the property * @return string the column name * @since 7.0.0 + * @deprecated Use Column attribute to map a property to a column */ public function propertyToColumn(string $property): string { $parts = preg_split('/(?=[A-Z])/', $property); + if (isset($this->_mappingPropertyToColumn[$property])) { + return $this->_mappingPropertyToColumn[$property]; + } + $column = ''; foreach ($parts as $part) { if ($column === '') { diff --git a/lib/public/AppFramework/Db/QBMapper.php b/lib/public/AppFramework/Db/QBMapper.php index d80bb5aec8b97..7b368abda5df3 100644 --- a/lib/public/AppFramework/Db/QBMapper.php +++ b/lib/public/AppFramework/Db/QBMapper.php @@ -7,11 +7,17 @@ */ namespace OCP\AppFramework\Db; +use BadMethodCallException; use Generator; +use OCP\AppFramework\Db\Attribute\Table; use OCP\DB\Exception; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\DB\Types; use OCP\IDBConnection; +use OCP\Server; +use OCP\Snowflake\IGenerator; +use ReflectionObject; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; /** * Simple parent class for inheriting your data access layer from. This class @@ -22,14 +28,8 @@ * @template T of Entity */ abstract class QBMapper { - /** @var string */ - protected $tableName; - /** @var string|class-string */ - protected $entityClass; - - /** @var IDBConnection */ - protected $db; + protected string $entityClass; /** * @param IDBConnection $db Instance of the Db abstraction layer @@ -38,10 +38,11 @@ abstract class QBMapper { * 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; - + public function __construct( + protected IDBConnection $db, + protected string $tableName, + ?string $entityClass = null, + ) { // 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) { @@ -51,7 +52,6 @@ public function __construct(IDBConnection $db, string $tableName, ?string $entit } } - /** * @return string the table name * @since 14.0.0 @@ -60,7 +60,6 @@ public function getTableName(): string { return $this->tableName; } - /** * Deletes an entity from the table * @@ -99,6 +98,18 @@ public function insert(Entity $entity): Entity { // be saved $properties = $entity->getUpdatedFields(); + if ($entity->id === null) { + $reflection = new ReflectionObject($entity); + $tables = $reflection->getAttributes(Table::class); + if (count($tables) > 0) { + /** @var Table $table */ + $table = $tables[0]; + if ($table->useSnowflakeId) { + $entity->id = Server::get(IGenerator::class); + } + } + } + $qb = $this->db->getQueryBuilder(); $qb->insert($this->tableName); @@ -359,8 +370,8 @@ protected function yieldEntities(IQueryBuilder $query): Generator { /** - * Returns an db result and throws exceptions when there are more or less - * results + * Returns a db result and throws exceptions when there are more or less + * results. * * @param IQueryBuilder $query * @return Entity the entity @@ -373,4 +384,70 @@ protected function yieldEntities(IQueryBuilder $query): Generator { protected function findEntity(IQueryBuilder $query): Entity { return $this->mapRowToEntity($this->findOneQuery($query)); } + + /** + * Finds all entities in the repository. + * + * @return \Generator + * @since 33.0.0 + */ + public function findAll(): \Generator { + return $this->findBy([]); + } + + /** + * Finds entities by a set of criteria. + * + * @param array $criteria + * @param array|null $orderBy + * @return \Generator + * @since 33.0.0 + */ + public function findBy(array $criteria, array|null $orderBy = null, int|null $limit = null, int|null $offset = null): \Generator { + $qb = $this->db->getQueryBuilder(); + $qb->select('*'); + foreach ($criteria as $field => $value) { + $qb->andWhere($qb->expr()->eq($field, $qb->createNamedParameter($field))); + } + foreach ($orderBy as $field => $direction) { + $qb->addOrderBy($qb->createNamedParameter($field), $direction); + } + + if ($limit !== null) { + $qb->setMaxResults($limit); + } + + if ($offset !== null) { + $qb->setFirstResult($offset); + } + + return $this->yieldEntities($qb); + } + + /** + * Finds a single entity by a set of criteria. + * + * @param array $criteria + * @param array|null $orderBy + * @return T|null + * @since 33.0.0 + */ + public function findOneBy(array $criteria, array|null $orderBy = null): Entity|null { + $qb = $this->db->getQueryBuilder(); + $qb->select('*'); + foreach ($criteria as $field => $value) { + $qb->andWhere($qb->expr()->eq($field, $qb->createNamedParameter($field))); + } + foreach ($orderBy as $field => $direction) { + $qb->addOrderBy($qb->createNamedParameter($field), $direction); + } + + $qb->setMaxResults(1); + + try { + return $this->findEntity($qb); + } catch (DoesNotExistException) { + return null; + } + } }