diff --git a/src/Annotations/OrmMappedArray.php b/src/Annotations/OrmMappedArray.php index 669638f..9c7eda3 100644 --- a/src/Annotations/OrmMappedArray.php +++ b/src/Annotations/OrmMappedArray.php @@ -34,20 +34,14 @@ final class OrmMappedArray * Value type. */ public readonly ValueType $valueType, - /** - * Whether to enable cache. - */ - public readonly bool $enableCache = true, /** * TTL of the cache, if null defaults to the value specified in the settings. * + * If zero disables caching. + * * @var int<0, max>|null */ public readonly ?int $cacheTtl = null, - /** - * Custom table name, if null is autogenerated. - */ - public readonly ?string $table = null, /** * Optimize table if more than this many megabytes are wasted, if null defaults to the value specified in the settings. * diff --git a/src/DbMapper.php b/src/DbMapper.php new file mode 100644 index 0000000..6ba3557 --- /dev/null +++ b/src/DbMapper.php @@ -0,0 +1,122 @@ +. + * + * @author Daniil Gentili + * @copyright 2016-2023 Daniil Gentili + * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 + * @link https://daniil.it/AsyncOrm AsyncOrm documentation + */ + +namespace danog\AsyncOrm; + +use Amp\Sync\LocalKeyedMutex; +use AssertionError; +use WeakMap; + +/** + * Async DB mapper. + * + * @template T as DbObject + * @template TKey as KeyType + */ +final readonly class DbMapper +{ + private readonly DbArray $arr; + private readonly LocalKeyedMutex $mutex; + private readonly WeakMap $inited; + /** + * Constructor. + * + * @param string $table Table name + * @param class-string Class of associated DbObject. + * @param Settings $settings Settings + * @param TKey $keyType Key type + * @param int<0, max>|null $cacheTtl TTL of the cache, if null defaults to the value specified in the settings, if zero disables caching. + * @param int<1, max>|null $optimizeIfWastedGtMb Optimize table if more than this many megabytes are wasted, if null defaults to the value specified in the settings. + * @param ?self $previous Previous instance, used for migrations. + */ + public function __construct( + private readonly string $table, + private readonly string $class, + private readonly Settings $settings, + KeyType $keyType, + ?int $cacheTtl = null, + ?int $optimizeIfWastedGtMb = null, + ?self $previous = null + ) { + if (\is_subclass_of($class, DbArray::class)) { + throw new AssertionError("$class must extend DbArray!"); + } + $this->inited = new WeakMap; + $this->mutex = new LocalKeyedMutex; + $config = new FieldConfig( + $table, + $settings, + $keyType, + ValueType::OBJECT, + $cacheTtl, + $optimizeIfWastedGtMb + ); + $this->arr = $config->get($previous?->arr); + if ($previous !== null) { + foreach ($previous->inited as $key => $obj) { + $obj->__initDb($this->table, $this->settings); + $this->inited[$key] = $obj; + } + } + } + + /** + * @param (TKey is KeyType::STRING ? string : (TKey is KeyType::INT ? int : string|int)) $key + * + * @return T + */ + public function create(string|int $key): DbObject + { + $lock = $this->mutex->acquire((string) $key); + try { + if (isset($this->inited[$key])) { + throw new AssertionError("An object under the key $key already exists!"); + } + $obj = new $this->class; + $obj->__initDb($this->table, $this->settings); + $this->arr[$key] = $obj; + $this->inited[$key] = $obj; + return $obj; + } finally { + $lock->release(); + } + } + + /** + * Find DbObject by key. + * + * @param (TKey is KeyType::STRING ? string : (TKey is KeyType::INT ? int : string|int)) $key + * + * @return ?T + */ + public function find(string|int $key): ?DbObject + { + $lock = $this->mutex->acquire((string) $key); + try { + if (isset($this->inited[$key])) { + return $this->inited[$key]; + } + $obj = $this->arr[$key]; + if ($obj !== null) { + $obj->__initDb($this->table, $this->settings); + $this->inited[$key] = $obj; + } + return $obj; + } finally { + $lock->release(); + } + } +} diff --git a/src/DbPropertiesTrait.php b/src/DbObject.php similarity index 65% rename from src/DbPropertiesTrait.php rename to src/DbObject.php index fe1cc96..0821145 100644 --- a/src/DbPropertiesTrait.php +++ b/src/DbObject.php @@ -19,29 +19,22 @@ namespace danog\AsyncOrm; use danog\AsyncOrm\Annotations\OrmMappedArray; -use danog\AsyncOrm\Internal\DbPropertiesFactory; +use danog\AsyncOrm\Settings\DriverSettings; +use danog\AsyncOrm\Settings\Mysql; use ReflectionClass; use function Amp\async; use function Amp\Future\await; -/** - * Include this trait and call DbPropertiesTrait::initDb to use AsyncOrm's database backend for properties marked by the OrmMappedArray property. - */ -trait DbPropertiesTrait +abstract class DbObject { /** * Initialize database instance. * * @internal */ - public function initDb(Settings $dbSettings): void + final public function __initDb(string $table, Settings $settings): void { - $prefix = $this->getDbPrefix(); - - $className = \explode('\\', static::class); - $className = \end($className); - $promises = []; foreach ((new ReflectionClass(static::class))->getProperties() as $property) { $attr = $property->getAttributes(OrmMappedArray::class); @@ -49,21 +42,34 @@ trait DbPropertiesTrait continue; } $attr = $attr[0]->newInstance(); - $table = $prefix.'_'; - $table .= $attr->table ?? "{$className}_{$property}"; - $promises[$property] = async(DbPropertiesFactory::get(...), $dbSettings, $table, $attr, $this->{$property} ?? null); + + $ttl = $attr->cacheTtl; + $optimize = $attr->optimizeIfWastedGtMb; + if ($settings instanceof DriverSettings) { + $ttl ??= $settings->cacheTtl; + + if ($settings instanceof Mysql) { + $optimize ??= $settings->optimizeIfWastedGtMb; + } + } + + $config = new FieldConfig( + $table.'_'.$property->getName(), + $settings, + $attr->keyType, + $attr->valueType, + $ttl, + $optimize, + ); + + $promises[$property] = async( + $config->get(...), + $this->{$property} ?? null + ); } $promises = await($promises); foreach ($promises as $key => $data) { $this->{$key} = $data; } } - - /** - * Returns the prefix to use for table names. - */ - protected function getTablePrefix(): string - { - return ''; - } } diff --git a/src/FieldConfig.php b/src/FieldConfig.php index 08b4aed..629e0f4 100644 --- a/src/FieldConfig.php +++ b/src/FieldConfig.php @@ -2,17 +2,52 @@ namespace danog\AsyncOrm; -use danog\AsyncOrm\Annotations\OrmMappedArray; +use danog\AsyncOrm\Internal\Driver\CachedArray; /** * Contains configuration for a single ORM field. */ -final class FieldConfig +final readonly class FieldConfig { public function __construct( + /** + * Table name. + */ public readonly string $table, - public readonly OrmMappedArray $annotation, + /** + * Settings. + */ public readonly Settings $settings, + /** + * Key type. + */ + public readonly KeyType $keyType, + /** + * Value type. + */ + public readonly ValueType $valueType, + /** + * TTL of the cache, if zero disables caching. + * + * @var int<0, max> + */ + public readonly int $cacheTtl, + /** + * Optimize table if more than this many megabytes are wasted, if null disables optimization. + * + * @var int<1, max>|null + */ + public readonly ?int $optimizeIfWastedGtMb, ) { } + + /** @internal */ + public function get(?DbArray $previous = null): DbArray + { + if ($this->cacheTtl === 0) { + return $this->settings->getDriverClass()::getInstance($this, $previous); + } + + return CachedArray::getInstance($this, $previous); + } } diff --git a/src/Internal/DbPropertiesFactory.php b/src/Internal/DbPropertiesFactory.php deleted file mode 100644 index 7c47b13..0000000 --- a/src/Internal/DbPropertiesFactory.php +++ /dev/null @@ -1,71 +0,0 @@ -. - * - * @author Daniil Gentili - * @author Alexander Pankratov - * @copyright 2016-2024 Daniil Gentili - * @copyright 2016-2024 Alexander Pankratov - * @license https://opensource.org/licenses/AGPL-3.0 AGPLv3 - * @link https://daniil.it/AsyncOrm AsyncOrm documentation - */ - -namespace danog\AsyncOrm\Internal; - -use danog\AsyncOrm\Annotations\OrmMappedArray; -use danog\AsyncOrm\DbArray; -use danog\AsyncOrm\FieldConfig; -use danog\AsyncOrm\Internal\Driver\CachedArray; -use danog\AsyncOrm\Settings; -use danog\AsyncOrm\Settings\DriverSettings; -use danog\AsyncOrm\Settings\Mysql; - -/** - * This factory class initializes the correct database backend for AsyncOrm. - * - * @internal - */ -final class DbPropertiesFactory -{ - public static function get(Settings $dbSettings, string $table, OrmMappedArray $config, ?DbArray $previous = null): DbArray - { - $dbSettings = clone $dbSettings; - - $ttl = $config->cacheTtl; - $optimize = $config->optimizeIfWastedGtMb; - if ($dbSettings instanceof DriverSettings) { - $ttl ??= $dbSettings->cacheTtl; - - if ($dbSettings instanceof Mysql) { - $optimize ??= $config->optimizeIfWastedGtMb; - } - - $config = new OrmMappedArray( - $config->keyType, - $config->valueType, - $ttl === 0 ? false : $config->enableCache, - $ttl, - $config->table, - $optimize - ); - } - - $config = new FieldConfig( - $table, - $config, - $dbSettings, - ); - - if (!$config->annotation->enableCache) { - return $config->settings->getDriverClass()::getInstance($config, $previous); - } - - return CachedArray::getInstance($config, $previous); - } -} diff --git a/src/OrmObject.php b/src/OrmObject.php new file mode 100644 index 0000000..e69de29 diff --git a/src/Settings/Mysql.php b/src/Settings/Mysql.php index d9816f0..3e957f0 100644 --- a/src/Settings/Mysql.php +++ b/src/Settings/Mysql.php @@ -31,7 +31,7 @@ final readonly class Mysql extends SqlSettings { /** * @param Serializer $serializer to use for object and mixed type values. - * @param int<0, max> $cacheTtl Cache TTL in seconds + * @param int<0, max> $cacheTtl Cache TTL in seconds, if 0 disables caching. * @param int<1, max> $maxConnections Maximum connection limit * @param int<1, max> $idleTimeout Idle timeout */