This commit is contained in:
Daniil Gentili 2024-03-26 18:32:26 +01:00
parent 4578655304
commit 53c536bf9e
24 changed files with 120 additions and 113 deletions

View File

@ -22,6 +22,7 @@ use Attribute;
use danog\AsyncOrm\KeyType;
use danog\AsyncOrm\ValueType;
/** @api */
#[Attribute(Attribute::TARGET_PROPERTY)]
final class OrmMappedArray
{

View File

@ -30,9 +30,16 @@ use Traversable;
* @implements ArrayAccess<TKey, TValue>
* @implements Traversable<TKey, TValue>
* @implements IteratorAggregate<TKey, TValue>
*
* @api
*/
abstract class DbArray implements Countable, ArrayAccess, Traversable, IteratorAggregate
{
/**
* Check if element exists.
*
* @param TKey $key
*/
final public function isset(string|int $key): bool
{
return $this->get($key) !== null;
@ -43,19 +50,19 @@ abstract class DbArray implements Countable, ArrayAccess, Traversable, IteratorA
return $this->get($offset);
}
final public function offsetExists(mixed $index): bool
final public function offsetExists(mixed $offset): bool
{
return $this->isset($index);
return $this->isset($offset);
}
final public function offsetSet(mixed $index, mixed $value): void
final public function offsetSet(mixed $offset, mixed $value): void
{
$this->set($index, $value);
$this->set($offset, $value);
}
final public function offsetUnset(mixed $index): void
final public function offsetUnset(mixed $offset): void
{
$this->unset($index);
$this->unset($offset);
}
public function getArrayCopy(): array

View File

@ -19,7 +19,9 @@
namespace danog\AsyncOrm;
use danog\AsyncOrm\Annotations\OrmMappedArray;
use danog\AsyncOrm\Internal\Containers\ObjectContainer;
use danog\AsyncOrm\Internal\Driver\CachedArray;
use danog\AsyncOrm\Internal\Driver\ObjectArray;
use danog\AsyncOrm\Settings\DriverSettings;
use danog\AsyncOrm\Settings\Mysql;
use ReflectionClass;
@ -27,11 +29,10 @@ use ReflectionClass;
use function Amp\async;
use function Amp\Future\await;
/** @api */
abstract class DbObject
{
/** @var list<CachedArray> */
private array $properties;
private DbArray $mapper;
private ObjectContainer $mapper;
private string|int $key;
/**
@ -39,55 +40,10 @@ abstract class DbObject
*
* @internal
*/
final public function initDb(DbArray $mapper, string|int $key, FieldConfig $config): void
final public function initDb(ObjectContainer $mapper, string|int $key, FieldConfig $config): void
{
$this->mapper = $mapper;
$this->key = $key;
$promises = [];
foreach ((new ReflectionClass(static::class))->getProperties() as $property) {
$attr = $property->getAttributes(OrmMappedArray::class);
if (!$attr) {
continue;
}
$attr = $attr[0]->newInstance();
$settings = $config->settings;
if ($settings instanceof DriverSettings) {
$ttl = $attr->cacheTtl ?? $settings->cacheTtl;
if ($ttl !== $settings->cacheTtl) {
$settings = new $settings(\array_merge(
(array) $settings,
['cacheTtl' => $ttl]
));
}
if ($settings instanceof Mysql) {
$optimize = $attr->optimizeIfWastedGtMb ?? $settings->optimizeIfWastedGtMb;
if ($optimize !== $settings->optimizeIfWastedGtMb) {
$settings = new $settings(\array_merge(
(array) $settings,
['optimizeIfWastedGtMb' => $optimize]
));
}
}
}
$config = new FieldConfig(
$config->table.'_'.($attr->tablePostfix ?? $property->getName()),
$settings,
$attr->keyType,
$attr->valueType,
);
$promises[] = async(function () use ($config, $property) {
$v = $config->build($property->getValue());
$property->setValue($v);
if ($v instanceof CachedArray) {
$this->properties []= $v;
}
});
}
await($promises);
}
/**
@ -95,10 +51,21 @@ abstract class DbObject
*/
public function save(): void
{
$promises = [async($this->mapper->set(...), $this->key, $this)];
foreach ($this->properties as $v) {
$promises []= async($v->flushCache(...));
}
await($promises);
$this->onBeforeSave();
$this->mapper->inner->set($this->key, $this);
$this->onAfterSave();
}
/**
* Method invoked before saving the object.
*/
protected function onBeforeSave(): void {
}
/**
* Method invoked after saving the object.
*/
protected function onAfterSave(): void {
}
}

View File

@ -35,6 +35,8 @@ use function Amp\Future\await;
* @consistent-constructor
*
* @extends DbArray<TKey, TValue>
*
* @api
*/
abstract class DriverArray extends DbArray
{
@ -44,7 +46,8 @@ abstract class DriverArray extends DbArray
public static function getInstance(FieldConfig $config, DbArray|null $previous): DbArray
{
if ($previous::class === static::class
if ($previous !== null
&& $previous::class === static::class
&& $previous->config == $config
) {
return $previous;

View File

@ -28,7 +28,8 @@ use danog\AsyncOrm\Settings\Database\Memory;
*
* @template TKey as array-key
* @template TValue
* @DbArray DbArray<TKey, TValue>
* @extends DbArray<TKey, TValue>
* @api
*/
final class MemoryArray extends DbArray
{
@ -37,7 +38,7 @@ final class MemoryArray extends DbArray
) {
}
public static function getInstance(FieldConfig $settings, DbArray|null $previous): DbArray
public static function getInstance(FieldConfig $config, DbArray|null $previous): DbArray
{
if ($previous instanceof self) {
return $previous;

View File

@ -29,6 +29,7 @@ use danog\AsyncOrm\Serializer;
* @template TKey as array-key
* @template TValue
* @extends DriverArray<TKey, TValue>
* @api
*/
abstract class SqlArray extends DriverArray
{

View File

@ -3,6 +3,7 @@
namespace danog\AsyncOrm;
use danog\AsyncOrm\Internal\Driver\CachedArray;
use danog\AsyncOrm\Internal\Driver\ObjectArray;
use danog\AsyncOrm\Settings\DriverSettings;
use danog\AsyncOrm\Settings\Memory;
@ -40,9 +41,13 @@ final readonly class FieldConfig
|| (
$this->settings instanceof DriverSettings
&& $this->settings->cacheTtl === 0
)) {
)
) {
return $this->settings->getDriverClass()::getInstance($this, $previous);
}
if ($this->valueType === ValueType::OBJECT) {
return ObjectArray::getInstance($this, $previous);
}
return CachedArray::getInstance($this, $previous);
}

View File

@ -61,7 +61,7 @@ final class CacheContainer
public function startCacheCleanupLoop(int $cacheTtl): void
{
$this->cacheTtl = $cacheTtl;
if ($this->cacheCleanupId) {
if ($this->cacheCleanupId !== null) {
EventLoop::cancel($this->cacheCleanupId);
}
$this->cacheCleanupId = EventLoop::repeat(
@ -71,7 +71,7 @@ final class CacheContainer
}
public function stopCacheCleanupLoop(): void
{
if ($this->cacheCleanupId) {
if ($this->cacheCleanupId !== null) {
EventLoop::cancel($this->cacheCleanupId);
$this->cacheCleanupId = null;
}
@ -87,6 +87,7 @@ final class CacheContainer
}
$result = $this->inner->offsetGet($index);
/** @psalm-suppress ParadoxicalCondition Concurrency */
if (isset($this->ttl[$index])) {
if ($this->ttl[$index] !== true) {
$this->ttl[$index] = \time() + $this->cacheTtl;

View File

@ -22,6 +22,7 @@ use Amp\Sync\LocalMutex;
use danog\AsyncOrm\DbArray;
use danog\AsyncOrm\DbObject;
use danog\AsyncOrm\FieldConfig;
use danog\AsyncOrm\Internal\Driver\ObjectArray;
use Revolt\EventLoop;
use Traversable;
@ -61,7 +62,7 @@ final class ObjectContainer
public function startCacheCleanupLoop(int $cacheTtl): void
{
$this->cacheTtl = $cacheTtl;
if ($this->cacheCleanupId) {
if ($this->cacheCleanupId !== null) {
EventLoop::cancel($this->cacheCleanupId);
}
$this->cacheCleanupId = EventLoop::repeat(
@ -71,7 +72,7 @@ final class ObjectContainer
}
public function stopCacheCleanupLoop(): void
{
if ($this->cacheCleanupId) {
if ($this->cacheCleanupId !== null) {
EventLoop::cancel($this->cacheCleanupId);
$this->cacheCleanupId = null;
}
@ -84,7 +85,7 @@ final class ObjectContainer
$ref = $obj->reference->get();
if ($ref !== null) {
$obj->ttl = \time() + $this->cacheTtl;
return $obj;
return $ref;
}
unset($this->cache[$index]);
}
@ -95,7 +96,7 @@ final class ObjectContainer
}
\assert($result instanceof DbObject);
$result->initDb($this->inner, $index, $this->config);
$result->initDb($this, $index, $this->config);
$this->cache[$index] = new ObjectReference($result, \time() + $this->cacheTtl);
@ -107,9 +108,9 @@ final class ObjectContainer
if (isset($this->cache[$key]) && $this->cache[$key]->reference->get() === $value) {
return;
}
$value->initDb($this->inner, $key, $this->config);
$value->initDb($this, $key, $this->config);
$this->cache[$key] = new ObjectReference($value, \time() + $this->cacheTtl);
$this->inner->set($key, $value);
$value->save();
}
public function unset(string|int $key): void
@ -122,6 +123,7 @@ final class ObjectContainer
{
$this->flushCache();
foreach ($this->inner->getIterator() as $key => $value) {
assert($value instanceof DbObject);
if (isset($this->cache[$key])) {
$obj = $this->cache[$key];
$ref = $obj->reference->get();
@ -131,7 +133,7 @@ final class ObjectContainer
continue;
}
}
$value->initDb($this->inner, $key, $this->config);
$value->initDb($this, $key, $this->config);
$this->cache[$key] = new ObjectReference($value, \time() + $this->cacheTtl);
yield $value;
}

View File

@ -55,7 +55,7 @@ final class CachedArray extends DbArray
$previous->cache->flushCache();
return $previous->cache->inner;
}
$previous->cache->startCacheCleanupLoop($config->cacheTtl);
$previous->cache->startCacheCleanupLoop($config->settings->cacheTtl);
return $previous;
}
@ -84,9 +84,9 @@ final class CachedArray extends DbArray
$this->cache->clear();
}
public function get(mixed $index): mixed
public function get(mixed $key): mixed
{
return $this->cache->get($index);
return $this->cache->get($key);
}
public function set(string|int $key, mixed $value): void

View File

@ -128,12 +128,12 @@ final class MysqlArray extends SqlArray
"
);
$keyType = match ($config->annotation->keyType) {
$keyType = match ($config->keyType) {
KeyType::STRING_OR_INT => "VARCHAR(255)",
KeyType::STRING => "VARCHAR(255)",
KeyType::INT => "BIGINT",
};
$valueType = match ($config->annotation->valueType) {
$valueType = match ($config->valueType) {
ValueType::INT => "BIGINT",
ValueType::STRING => "VARCHAR(255)",
default => "MEDIUMBLOB",
@ -143,7 +143,7 @@ final class MysqlArray extends SqlArray
CREATE TABLE IF NOT EXISTS `{$config->table}`
(
`key` $keyType PRIMARY KEY NOT NULL,
`value` $valueType NOT NULL,
`value` $valueType NOT NULL
)
ENGINE = InnoDB
CHARACTER SET 'utf8mb4'
@ -163,6 +163,7 @@ final class MysqlArray extends SqlArray
$expected = $valueType;
} else {
$this->db->query("ALTER TABLE `{$config->table}` DROP `$key`");
continue;
}
if ($expected !== $type || $null !== 'NO') {
$this->db->query("ALTER TABLE `{$config->table}` MODIFY `$key` $expected NOT NULL");

View File

@ -56,7 +56,7 @@ final class ObjectArray extends DbArray
$previous->cache->flushCache();
return $previous->cache->inner;
}
$previous->cache->startCacheCleanupLoop($config->cacheTtl);
$previous->cache->startCacheCleanupLoop($config->settings->cacheTtl);
return $previous;
}
@ -80,9 +80,9 @@ final class ObjectArray extends DbArray
$this->cache->clear();
}
public function get(mixed $index): mixed
public function get(mixed $key): mixed
{
return $this->cache->get($index);
return $this->cache->get($key);
}
public function set(string|int $key, mixed $value): void

View File

@ -70,7 +70,7 @@ class PostgresArray extends SqlArray
}
$connection->close();
self::$connections[$dbKey] = new PostgresConnectionPool($settings->config, $settings->maxConnections, $settings->idleTimeoutgetIdleTimeout());
self::$connections[$dbKey] = new PostgresConnectionPool($settings->config, $settings->maxConnections, $settings->idleTimeout);
}
} finally {
EventLoop::queue($lock->release(...));
@ -78,17 +78,17 @@ class PostgresArray extends SqlArray
$connection = self::$connections[$dbKey];
$keyType = match ($config->annotation->keyType) {
$keyType = match ($config->keyType) {
KeyType::STRING_OR_INT => "VARCHAR(255)",
KeyType::STRING => "VARCHAR(255)",
KeyType::INT => "BIGINT",
};
$valueType = match ($config->annotation->valueType) {
$valueType = match ($config->valueType) {
ValueType::INT => "BIGINT",
ValueType::STRING => "VARCHAR(255)",
default => "BYTEA",
};
$serializer = match ($config->annotation->valueType) {
$serializer = match ($config->valueType) {
ValueType::INT, ValueType::STRING => $serializer,
default => new ByteaSerializer($serializer)
};
@ -114,6 +114,7 @@ class PostgresArray extends SqlArray
$expected = $valueType;
} else {
$this->db->query("ALTER TABLE \"bytea_{$config->table}\" DROP \"$key\"");
continue;
}
if ($expected !== $type || $null !== 'NO') {
$this->db->query("ALTER TABLE \"bytea_{$config->table}\" MODIFY \"$key\" $expected NOT NULL");

View File

@ -51,7 +51,7 @@ final class RedisArray extends DriverArray
public function __construct(FieldConfig $config, Serializer $serializer)
{
if ($serializer instanceof Passthrough && $config->annotation->valueType === ValueType::INT) {
if ($serializer instanceof Passthrough && $config->valueType === ValueType::INT) {
$serializer = new IntString;
}
parent::__construct($config, $serializer);
@ -60,7 +60,6 @@ final class RedisArray extends DriverArray
\assert($config->settings instanceof Redis);
$dbKey = $config->settings->getDbIdentifier();
$lock = self::$mutex->acquire($dbKey);
\assert($config->settings instanceof Redis);
try {
if (!isset(self::$connections[$dbKey])) {
@ -84,8 +83,10 @@ final class RedisArray extends DriverArray
foreach ($request as $oldKey) {
$newKey = $to.\substr($oldKey, $lenK);
$value = $this->db->get($oldKey);
$this->db->set($newKey, $value);
$this->db->delete($oldKey);
if ($value !== null) {
$this->db->set($newKey, $value);
$this->db->delete($oldKey);
}
}
}
@ -110,11 +111,11 @@ final class RedisArray extends DriverArray
$this->db->set($this->rKey((string) $key), $this->serializer->serialize($value));
}
public function get(mixed $offset): mixed
public function get(mixed $key): mixed
{
$offset = (string) $offset;
$key = (string) $key;
$value = $this->db->get($this->rKey($offset));
$value = $this->db->get($this->rKey($key));
if ($value !== null) {
$value = $this->serializer->deserialize($value);

View File

@ -30,6 +30,7 @@ final class ByteaSerializer implements Serializer
}
public function serialize(mixed $value): mixed
{
/** @psalm-suppress MixedArgument */
return new PostgresByteA($this->inner->serialize($value));
}
public function deserialize(mixed $value): mixed

View File

@ -20,7 +20,11 @@ namespace danog\AsyncOrm\Serializer;
use danog\AsyncOrm\Serializer;
/** Igbinary serializer. */
/**
* Igbinary serializer.
*
* @api
*/
final class Igbinary implements Serializer
{
public function serialize(mixed $value): mixed
@ -29,6 +33,7 @@ final class Igbinary implements Serializer
}
public function deserialize(mixed $value): mixed
{
assert(is_string($value));
return \igbinary_unserialize($value);
}
}

View File

@ -20,7 +20,11 @@ namespace danog\AsyncOrm\Serializer;
use danog\AsyncOrm\Serializer;
/** Integer casting serializer */
/**
* Integer casting serializer
*
* @api
*/
final class IntString implements Serializer
{
public function serialize(mixed $value): mixed

View File

@ -20,7 +20,11 @@ namespace danog\AsyncOrm\Serializer;
use danog\AsyncOrm\Serializer;
/** JSON serializer */
/**
* JSON serializer
*
* @api
*/
final class Json implements Serializer
{
public function serialize(mixed $value): mixed
@ -29,6 +33,7 @@ final class Json implements Serializer
}
public function deserialize(mixed $value): mixed
{
assert(is_string($value));
return \json_decode($value, true, flags: JSON_THROW_ON_ERROR);
}
}

View File

@ -20,7 +20,11 @@ namespace danog\AsyncOrm\Serializer;
use danog\AsyncOrm\Serializer;
/** Native serializer */
/**
* Native serializer
*
* @api
*/
final class Native implements Serializer
{
public function serialize(mixed $value): mixed
@ -29,6 +33,7 @@ final class Native implements Serializer
}
public function deserialize(mixed $value): mixed
{
assert(is_string($value));
return \unserialize($value);
}
}

View File

@ -20,7 +20,11 @@ namespace danog\AsyncOrm\Serializer;
use danog\AsyncOrm\Serializer;
/** Passthrough serializer */
/**
* Passthrough serializer
*
* @api
*/
final class Passthrough implements Serializer
{
public function serialize(mixed $value): mixed

View File

@ -30,6 +30,7 @@ use danog\AsyncOrm\Serializer;
final readonly class Mysql extends SqlSettings
{
/**
* @api
* @param Serializer $serializer to use for object and mixed type values.
* @param int<0, max> $cacheTtl Cache TTL in seconds, if 0 disables caching.
* @param int<1, max> $maxConnections Maximum connection limit

View File

@ -17,8 +17,8 @@
namespace danog\AsyncOrm\Settings;
use Amp\Postgres\PostgresConfig;
use Amp\Serialization\Serializer;
use danog\AsyncOrm\Internal\Driver\PostgresArray;
use danog\AsyncOrm\Serializer;
/**
* Postgres backend settings.
@ -27,6 +27,7 @@ use danog\AsyncOrm\Internal\Driver\PostgresArray;
final readonly class Postgres extends SqlSettings
{
/**
* @api
* @param Serializer $serializer to use for object and mixed type values.
* @param int<0, max> $cacheTtl Cache TTL in seconds
* @param int<1, max> $maxConnections Maximum connection limit

View File

@ -26,6 +26,8 @@ use danog\AsyncOrm\Serializer;
final readonly class Redis extends DriverSettings
{
/**
* @api
*
* @param Serializer $serializer to use for object and mixed type values.
* @param int<0, max> $cacheTtl Cache TTL in seconds
*/
@ -34,7 +36,7 @@ final readonly class Redis extends DriverSettings
Serializer $serializer,
int $cacheTtl = self::DEFAULT_CACHE_TTL,
) {
parent::__construct($cacheTtl, $serializer);
parent::__construct($serializer, $cacheTtl);
}
/** @internal */
public function getDbIdentifier(): string

View File

@ -28,18 +28,6 @@ abstract readonly class SqlSettings extends DriverSettings
{
final public const DEFAULT_SQL_MAX_CONNECTIONS = 100;
final public const DEFAULT_SQL_IDLE_TIMEOUT = 60;
/**
* Database password.
*/
public string $database;
/**
* Database username.
*/
public string $username;
/**
* Database password.
*/
public string $password;
/**
* Maximum connection limit.
*
@ -76,6 +64,6 @@ abstract readonly class SqlSettings extends DriverSettings
{
$host = $this->config->getHost();
$port = $this->config->getPort();
return "$host:$port:".$this->config->getDatabase();
return "$host:$port:".(string)$this->config->getDatabase();
}
}