mirror of
https://github.com/nextcloud/server.git
synced 2026-06-13 18:50:47 -04:00
feat(entity): Attributes for Entity
Signed-off-by: Carl Schwan <carl.schwan@nextcloud.com>
This commit is contained in:
parent
df8d838186
commit
14cd485ce2
6 changed files with 226 additions and 70 deletions
|
|
@ -8,9 +8,14 @@ declare(strict_types=1);
|
|||
*/
|
||||
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 @@ use OCP\AppFramework\Db\Entity;
|
|||
* @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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 @@ use OCP\AppFramework\Db\Entity;
|
|||
* @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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
28
lib/public/AppFramework/Db/Attribute/Column.php
Normal file
28
lib/public/AppFramework/Db/Attribute/Column.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
|
||||
* SPDX-FileContributor: Carl Schwan
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCP\AppFramework\Db\Attribute;
|
||||
|
||||
use Attribute;
|
||||
use OCP\AppFramework\Attribute\Consumable;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\DB\Types;
|
||||
|
||||
#[Attribute(Attribute::TARGET_PROPERTY)]
|
||||
#[Consumable(since: '33.0.0')]
|
||||
final readonly class Column {
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string|null $type,
|
||||
public int|null $length = null,
|
||||
public bool $nullable = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
26
lib/public/AppFramework/Db/Attribute/Table.php
Normal file
26
lib/public/AppFramework/Db/Attribute/Table.php
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH
|
||||
* SPDX-FileContributor: Carl Schwan
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCP\AppFramework\Db\Attribute;
|
||||
|
||||
use Attribute;
|
||||
use OCP\AppFramework\Attribute\Consumable;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\DB\Types;
|
||||
|
||||
#[Attribute(Attribute::TARGET_CLASS)]
|
||||
#[Consumable(since: '33.0.0')]
|
||||
final readonly class Table {
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public bool $useSnowflakeId = false,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
*/
|
||||
namespace OCP\AppFramework\Db;
|
||||
|
||||
use OCP\AppFramework\Db\Attribute\Column;
|
||||
use OCP\AppFramework\Db\Attribute\Table;
|
||||
use OCP\DB\Types;
|
||||
|
||||
use function lcfirst;
|
||||
|
|
@ -19,12 +21,44 @@ use function substr;
|
|||
* @psalm-consistent-constructor
|
||||
*/
|
||||
abstract class Entity {
|
||||
/** @var int $id */
|
||||
public $id = null;
|
||||
#[Column(name: 'id', type: Types::BIGINT)]
|
||||
public string|int|null $id = null;
|
||||
|
||||
/** @var array<string, bool> */
|
||||
private array $_updatedFields = [];
|
||||
|
||||
/** @var array<string, \OCP\DB\Types::*> */
|
||||
private array $_fieldTypes = ['id' => 'integer'];
|
||||
private array $_fieldTypes = ['id' => Types::INTEGER];
|
||||
|
||||
/** @var array<string, string> */
|
||||
private array $_mappingColumnToProperty = [];
|
||||
|
||||
/** @var array<string, string> */
|
||||
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 @@ abstract class Entity {
|
|||
// 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 @@ abstract class Entity {
|
|||
* @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 @@ abstract class Entity {
|
|||
* @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 === '') {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,17 @@ declare(strict_types=1);
|
|||
*/
|
||||
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 @@ use OCP\IDBConnection;
|
|||
* @template T of Entity
|
||||
*/
|
||||
abstract class QBMapper {
|
||||
/** @var string */
|
||||
protected $tableName;
|
||||
|
||||
/** @var string|class-string<T> */
|
||||
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 @@ abstract class QBMapper {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return string the table name
|
||||
* @since 14.0.0
|
||||
|
|
@ -60,7 +60,6 @@ abstract class QBMapper {
|
|||
return $this->tableName;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes an entity from the table
|
||||
*
|
||||
|
|
@ -99,6 +98,18 @@ abstract class QBMapper {
|
|||
// 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 @@ abstract class QBMapper {
|
|||
|
||||
|
||||
/**
|
||||
* 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 @@ abstract class QBMapper {
|
|||
protected function findEntity(IQueryBuilder $query): Entity {
|
||||
return $this->mapRowToEntity($this->findOneQuery($query));
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all entities in the repository.
|
||||
*
|
||||
* @return \Generator<T>
|
||||
* @since 33.0.0
|
||||
*/
|
||||
public function findAll(): \Generator {
|
||||
return $this->findBy([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds entities by a set of criteria.
|
||||
*
|
||||
* @param array<string, int|float|string> $criteria
|
||||
* @param array<string, 'asc'|'desc'>|null $orderBy
|
||||
* @return \Generator<T>
|
||||
* @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<string, int|float|string> $criteria
|
||||
* @param array<string, 'asc'|'desc'>|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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue