diff --git a/README.md b/README.md index d6c358b..176d275 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Cycle ORM Entity Behavior Identifier -[![Latest Stable Version](https://poser.pugx.org/cycle/entity-behavior-Identifier/version)](https://packagist.org/packages/cycle/entity-behavior-identifier) +[![Latest Stable Version](https://poser.pugx.org/cycle/entity-behavior-identifier/version)](https://packagist.org/packages/cycle/entity-behavior-identifier) [![Build Status](https://github.com/cycle/entity-behavior-identifier/workflows/build/badge.svg)](https://github.com/cycle/entity-behavior-identifier/actions) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/cycle/entity-behavior-identifier/badges/quality-score.png?b=1.x)](https://scrutinizer-ci.com/g/cycle/entity-behavior-identifier/?branch=1.x) [![Codecov](https://codecov.io/gh/cycle/entity-behavior-identifier/graph/badge.svg)](https://codecov.io/gh/cycle/entity-behavior) @@ -19,9 +18,95 @@ composer require cycle/entity-behavior-identifier ## Snowflake Examples -**Snowflake:** A distributed ID generation system developed by Twitter that produces 64-bit unique, sortable identifiers. Each ID encodes a timestamp, machine ID, and sequence number, enabling high-throughput, ordered ID creation suitable for large-scale distributed applications. +### Generic +A flexible Snowflake format that can use a node identifier and any epoch offset, suitable for various applications requiring unique identifiers. Default values for `node` and `epochOffset` can be defined globally via the `\Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeGeneric::setDefaults()` method. -> **Note:** Support for Snowflake identifiers will arrive soon, stay tuned. +```php +use Cycle\Annotated\Annotation\Column; +use Cycle\Annotated\Annotation\Entity; +use Cycle\ORM\Entity\Behavior\Identifier; +use Ramsey\Identifier\Snowflake; + +#[Entity] +#[Identifier\SnowflakeGeneric(field: 'id', node: 1, epochOffset: 1738265600000)] +class User +{ + #[Column(type: 'snowflake', primary: true)] + private Snowflake $id; +} +``` + +### Discord +Snowflake identifier for Discord's platform (voice, text, video), starting from epoch `2015-01-01`. Can incorporate a worker and process ID's to generate distinct Snowflakes. Default values for `workerId` and `processId` can be defined globally via the `\Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeDiscord::setDefaults()` method. + +```php +use Cycle\Annotated\Annotation\Column; +use Cycle\Annotated\Annotation\Entity; +use Cycle\ORM\Entity\Behavior\Identifier; +use Ramsey\Identifier\Snowflake; + +#[Entity] +#[Identifier\SnowflakeDiscord(field: 'id', workerId: 12, processId: 24)] +class User +{ + #[Column(type: 'snowflake', primary: true)] + private Snowflake $id; +} +``` + +### Instagram +Snowflake identifier for Instagram's photo and video sharing platform, with an epoch starting at `2011-08-24`. Can incorporate a shard ID to generate distinct Snowflakes. Default values for `shardId` can be defined globally via the `\Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeInstagram::setDefaults()` method. + +```php +use Cycle\Annotated\Annotation\Column; +use Cycle\Annotated\Annotation\Entity; +use Cycle\ORM\Entity\Behavior\Identifier; +use Ramsey\Identifier\Snowflake; + +#[Entity] +#[Identifier\SnowflakeInstagram(field: 'id', shardId: 16)] +class User +{ + #[Column(type: 'snowflake', primary: true)] + private Snowflake $id; +} +``` + +### Mastodon +Snowflake identifier for Mastodon's decentralized social network, generated within a database to ensure uniqueness and approximate order within 1ms. Can include a table name for distinct sequences per table; IDs are unique on a single database but not guaranteed across multiple machines. Default values for `tableName` can be defined globally via the `\Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeMastodon::setDefaults()` method. + +```php +use Cycle\Annotated\Annotation\Column; +use Cycle\Annotated\Annotation\Entity; +use Cycle\ORM\Entity\Behavior\Identifier; +use Ramsey\Identifier\Snowflake; + +#[Entity] +#[Identifier\SnowflakeMastodon(field: 'id', tableName: 'users')] +class User +{ + #[Column(type: 'snowflake', primary: true)] + private Snowflake $id; +} +``` + +### Twitter +Snowflake identifier for Twitter (X), beginning from `2010-11-04`. Can incorporate a machine ID to generate distinct Snowflakes. Default values for `machineId` can be defined globally via the `\Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeTwitter::setDefaults()` method. + +```php +use Cycle\Annotated\Annotation\Column; +use Cycle\Annotated\Annotation\Entity; +use Cycle\ORM\Entity\Behavior\Identifier; +use Ramsey\Identifier\Snowflake; + +#[Entity] +#[Identifier\SnowflakeTwitter(field: 'id', machineId: 30)] +class User +{ + #[Column(type: 'snowflake', primary: true)] + private Snowflake $id; +} +``` ## ULID Examples @@ -44,7 +129,8 @@ class User ## UUID Examples -**UUID Version 1 (Time-based):** Generated using the current timestamp and the MAC address of the computer, ensuring unique identification based on time and hardware. +### UUID Version 1 (Time-based) +Generated using the current timestamp and the MAC address of the computer, ensuring unique identification based on time and hardware. Default values for `node` and `clockSeq` can be defined globally via the `\Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid1::setDefaults()` method. ```php use Cycle\Annotated\Annotation\Column; @@ -61,7 +147,8 @@ class User } ``` -**UUID Version 2 (DCE Security):** Similar to version 1 but includes a local identifier such as a user ID or group ID, primarily used in DCE security contexts. +### UUID Version 2 (DCE Security) +Similar to version 1 but includes a local identifier such as a user ID or group ID, primarily used in DCE security contexts. Default values for `localDomain`, `localIdentifier`, `node` and `clockSeq` can be defined globally via the `\Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid2::setDefaults()` method. ```php use Cycle\Annotated\Annotation\Column; @@ -137,7 +224,8 @@ class User } ``` -**UUID Version 6 (Draft/Upcoming):** An experimental or proposed version focused on improving time-based UUIDs with more sortable properties (not yet widely adopted). +### UUID Version 6 (Draft/Upcoming) +An experimental or proposed version focused on improving time-based UUIDs with more sortable properties (not yet widely adopted). Default values for `node` and `clockSeq` can be defined globally via the `\Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid6::setDefaults()` method. ```php use Cycle\Annotated\Annotation\Column; @@ -171,7 +259,7 @@ class User } ``` -You can find more information about Entity behavior UUID [here](https://cycle-orm.dev/docs/entity-behaviors-identifier). +You can find more information about Entity behavior Identifier [here](https://cycle-orm.dev/docs/entity-behaviors-identifier). ## License: diff --git a/composer.lock b/composer.lock index dffb64d..d33972d 100644 --- a/composer.lock +++ b/composer.lock @@ -68,16 +68,16 @@ }, { "name": "cycle/database", - "version": "2.14.0", + "version": "2.15.0", "source": { "type": "git", "url": "https://github.com/cycle/database.git", - "reference": "876fbc2bc0d068f047388c0bd9b354e4d891af07" + "reference": "3d7ee3524b299c5897e2b03dc51bad2ddd609a90" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cycle/database/zipball/876fbc2bc0d068f047388c0bd9b354e4d891af07", - "reference": "876fbc2bc0d068f047388c0bd9b354e4d891af07", + "url": "https://api.github.com/repos/cycle/database/zipball/3d7ee3524b299c5897e2b03dc51bad2ddd609a90", + "reference": "3d7ee3524b299c5897e2b03dc51bad2ddd609a90", "shasum": "" }, "require": { @@ -157,20 +157,20 @@ "type": "github" } ], - "time": "2025-07-14T11:36:41+00:00" + "time": "2025-07-22T05:27:52+00:00" }, { "name": "cycle/entity-behavior", - "version": "1.6.0", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/cycle/entity-behavior.git", - "reference": "49b0c71485855f16193b0720d637d822d65f688c" + "reference": "0c8d84fb3eaa50ec426f336a158d62ad2b4a83b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cycle/entity-behavior/zipball/49b0c71485855f16193b0720d637d822d65f688c", - "reference": "49b0c71485855f16193b0720d637d822d65f688c", + "url": "https://api.github.com/repos/cycle/entity-behavior/zipball/0c8d84fb3eaa50ec426f336a158d62ad2b4a83b6", + "reference": "0c8d84fb3eaa50ec426f336a158d62ad2b4a83b6", "shasum": "" }, "require": { @@ -232,7 +232,7 @@ "type": "github" } ], - "time": "2025-07-14T19:37:04+00:00" + "time": "2025-07-22T05:27:05+00:00" }, { "name": "cycle/orm", @@ -2538,19 +2538,20 @@ }, { "name": "cycle/annotated", - "version": "v4.3.0", + "version": "v4.3.1", "source": { "type": "git", "url": "https://github.com/cycle/annotated.git", - "reference": "35890d8fe16b6a7a29cbacef5715d31b13b78212" + "reference": "f996d3ee0c22aa8f2c03dca5d693408f8b7fdbbe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cycle/annotated/zipball/35890d8fe16b6a7a29cbacef5715d31b13b78212", - "reference": "35890d8fe16b6a7a29cbacef5715d31b13b78212", + "url": "https://api.github.com/repos/cycle/annotated/zipball/f996d3ee0c22aa8f2c03dca5d693408f8b7fdbbe", + "reference": "f996d3ee0c22aa8f2c03dca5d693408f8b7fdbbe", "shasum": "" }, "require": { + "cycle/database": "^2.15", "cycle/orm": "^2.9.2", "cycle/schema-builder": "^2.11.1", "doctrine/inflector": "^2.0", @@ -2607,7 +2608,7 @@ "type": "github" } ], - "time": "2025-05-14T14:48:40+00:00" + "time": "2025-07-22T06:19:06+00:00" }, { "name": "danog/advanced-json-rpc", @@ -3200,16 +3201,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.83.0", + "version": "v3.84.0", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "b83916e79a6386a1ec43fdd72391aeb13b63282f" + "reference": "38dad0767bf2a9b516b976852200ae722fe984ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/b83916e79a6386a1ec43fdd72391aeb13b63282f", - "reference": "b83916e79a6386a1ec43fdd72391aeb13b63282f", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/38dad0767bf2a9b516b976852200ae722fe984ca", + "reference": "38dad0767bf2a9b516b976852200ae722fe984ca", "shasum": "" }, "require": { @@ -3293,7 +3294,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.83.0" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.84.0" }, "funding": [ { @@ -3301,7 +3302,7 @@ "type": "github" } ], - "time": "2025-07-14T15:41:41+00:00" + "time": "2025-07-15T18:21:57+00:00" }, { "name": "kelunik/certificate", diff --git a/src/Listener/BaseUuid.php b/src/Listener/BaseUuid.php new file mode 100644 index 0000000..046d31a --- /dev/null +++ b/src/Listener/BaseUuid.php @@ -0,0 +1,32 @@ +nullable || isset($event->state->getData()[$this->field])) { + return; + } + + $event->state->register($this->field, $this->createValue()); + } + + abstract protected function createValue(): \Ramsey\Identifier\Uuid; +} diff --git a/src/Listener/Snowflake.php b/src/Listener/Snowflake.php new file mode 100644 index 0000000..9f2adaf --- /dev/null +++ b/src/Listener/Snowflake.php @@ -0,0 +1,32 @@ +nullable || isset($event->state->getData()[$this->field])) { + return; + } + + $event->state->register($this->field, $this->createValue()); + } + + abstract protected function createValue(): \Ramsey\Identifier\Snowflake; +} diff --git a/src/Listener/SnowflakeDiscord.php b/src/Listener/SnowflakeDiscord.php new file mode 100644 index 0000000..510447f --- /dev/null +++ b/src/Listener/SnowflakeDiscord.php @@ -0,0 +1,76 @@ + */ + private static int $defaultWorkerId = 0; + + /** @var null|int<0, 281474976710655> */ + private static ?int $defaultProcessId = null; + + private DiscordSnowflakeFactory $factory; + + /** + * @param non-empty-string $field The name of the field to store the Snowflake identifier + * @param bool $nullable Indicates whether the Snowflake identifier can be null + * @param int<0, 281474976710655>|null $workerId A worker identifier to use when creating Snowflakes + * @param int<0, 281474976710655>|null $processId A process identifier to use when creating Snowflakes + */ + public function __construct( + string $field, + bool $nullable = false, + ?int $workerId = null, + ?int $processId = null, + ) { + $workerId ??= self::$defaultWorkerId; + $processId ??= $this->getProcessId(); + $this->factory = new DiscordSnowflakeFactory($workerId, $processId); + parent::__construct($field, $nullable); + } + + /** + * Set default worker and process IDs for Snowflake generation. + * + * @param null|int<0, 281474976710655> $workerId The worker ID to set. Null to use the default (0). + * @param null|int<0, 281474976710655> $processId The process ID to set. Null to use the current process ID. + */ + public static function setDefaults(?int $workerId, ?int $processId): void + { + if ($workerId !== null && ($workerId < 0 || $workerId > 281474976710655)) { + throw new \InvalidArgumentException('Worker ID must be between 0 and 281474976710655.'); + } + if ($processId !== null && ($processId < 0 || $processId > 281474976710655)) { + throw new \InvalidArgumentException('Process ID must be between 0 and 281474976710655.'); + } + + self::$defaultWorkerId = (int) $workerId; + self::$defaultProcessId = $processId; + } + + #[\Override] + protected function createValue(): DiscordSnowflake + { + return $this->factory->create(); + } + + /** + * Get the current process ID. + * + * @return int<0, 281474976710655> + */ + private function getProcessId(): int + { + return self::$defaultProcessId ??= \getmypid(); + } +} diff --git a/src/Listener/SnowflakeGeneric.php b/src/Listener/SnowflakeGeneric.php new file mode 100644 index 0000000..c20343b --- /dev/null +++ b/src/Listener/SnowflakeGeneric.php @@ -0,0 +1,64 @@ + */ + private static int $node = 0; + + private static Epoch|int $epochOffset = 0; + private GenericSnowflakeFactory $factory; + + /** + * @param non-empty-string $field The name of the field to store the Snowflake identifier + * @param bool $nullable Indicates whether the Snowflake identifier can be null + * @param int<0, 1023>|null $node A node identifier to use when creating Snowflakes + * @param Epoch|int|null $epochOffset The offset from the Unix Epoch in milliseconds + */ + public function __construct( + string $field, + bool $nullable = false, + ?int $node = null, + Epoch|int|null $epochOffset = null, + ) { + $node ??= self::$node; + $epochOffset ??= self::$epochOffset; + $this->factory = new GenericSnowflakeFactory($node, $epochOffset); + parent::__construct($field, $nullable); + } + + /** + * Set default node and epoch offset for Snowflake generation. + * + * @param null|int<0, 1023> $node The node ID to set. Null to use the default (0). + * @param Epoch|int|null $epochOffset The epoch offset to set. Null to use the default (0). + */ + public static function setDefaults(?int $node, Epoch|int|null $epochOffset): void + { + if ($node !== null && ($node < 0 || $node > 1023)) { + throw new \InvalidArgumentException('Node ID must be between 0 and 1023.'); + } + + self::$node = (int) $node; + if ($epochOffset !== null) { + self::$epochOffset = $epochOffset; + } + } + + #[\Override] + protected function createValue(): Snowflake + { + return $this->factory->create(); + } +} diff --git a/src/Listener/SnowflakeInstagram.php b/src/Listener/SnowflakeInstagram.php new file mode 100644 index 0000000..000a0ad --- /dev/null +++ b/src/Listener/SnowflakeInstagram.php @@ -0,0 +1,55 @@ + */ + private static int $shardId = 0; + + private InstagramSnowflakeFactory $factory; + + /** + * @param non-empty-string $field The name of the field to store the Snowflake identifier + * @param bool $nullable Indicates whether the Snowflake identifier can be null + * @param int<0, 1023>|null $shardId A shard identifier to use when creating Snowflakes + */ + public function __construct( + string $field, + bool $nullable = false, + ?int $shardId = null, + ) { + $shardId ??= self::$shardId; + $this->factory = new InstagramSnowflakeFactory($shardId); + parent::__construct($field, $nullable); + } + + /** + * Set default shard ID for Snowflake generation. + * + * @param null|int<0, 1023> $shardId The shard ID to set. Null to use the default (0). + */ + public static function setDefaults(?int $shardId): void + { + if ($shardId !== null && ($shardId < 0 || $shardId > 1023)) { + throw new \InvalidArgumentException('Shard ID must be between 0 and 1023.'); + } + + self::$shardId = (int) $shardId; + } + + #[\Override] + protected function createValue(): InstagramSnowflake + { + return $this->factory->create(); + } +} diff --git a/src/Listener/SnowflakeMastodon.php b/src/Listener/SnowflakeMastodon.php new file mode 100644 index 0000000..235c39d --- /dev/null +++ b/src/Listener/SnowflakeMastodon.php @@ -0,0 +1,51 @@ +factory = new MastodonSnowflakeFactory($tableName); + parent::__construct($field, $nullable); + } + + /** + * Set default table name for Snowflake generation. + * + * @param non-empty-string|null $tableName The table name to set. Null to use the default (null). + */ + public static function setDefaults(?string $tableName): void + { + self::$tableName = $tableName; + } + + #[\Override] + protected function createValue(): MastodonSnowflake + { + return $this->factory->create(); + } +} diff --git a/src/Listener/SnowflakeTwitter.php b/src/Listener/SnowflakeTwitter.php new file mode 100644 index 0000000..ce6a942 --- /dev/null +++ b/src/Listener/SnowflakeTwitter.php @@ -0,0 +1,55 @@ + */ + private static int $machineId = 0; + + private TwitterSnowflakeFactory $factory; + + /** + * @param non-empty-string $field The name of the field to store the Snowflake identifier + * @param bool $nullable Indicates whether the Snowflake identifier can be null + * @param int<0, 1023>|null $machineId A machine identifier to use when creating Snowflakes + */ + public function __construct( + string $field, + bool $nullable = false, + ?int $machineId = null, + ) { + $machineId ??= self::$machineId; + $this->factory = new TwitterSnowflakeFactory($machineId); + parent::__construct($field, $nullable); + } + + /** + * Set default machine ID for Snowflake generation. + * + * @param null|int<0, 1023> $machineId The machine ID to set. Null to use the default (0). + */ + public static function setDefaults(?int $machineId): void + { + if ($machineId !== null && ($machineId < 0 || $machineId > 1023)) { + throw new \InvalidArgumentException('Machine ID must be between 0 and 1023.'); + } + + self::$machineId = (int) $machineId; + } + + #[\Override] + protected function createValue(): TwitterSnowflake + { + return $this->factory->create(); + } +} diff --git a/src/Listener/Uuid1.php b/src/Listener/Uuid1.php index e24a0e3..5bc7dea 100644 --- a/src/Listener/Uuid1.php +++ b/src/Listener/Uuid1.php @@ -4,34 +4,53 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; -use Cycle\ORM\Entity\Behavior\Attribute\Listen; -use Cycle\ORM\Entity\Behavior\Event\Mapper\Command\OnCreate; -use Ramsey\Identifier\Uuid\UuidFactory; +use Ramsey\Identifier\Uuid\UuidV1Factory; -final class Uuid1 +/** + * Generates UUIDv1 identifiers for entities. + * You can set default node and clock sequence using the {@see setDefaults()} method. + */ +final class Uuid1 extends BaseUuid { + /** @var int<0, 281474976710655>|non-empty-string|null */ + private static int|string|null $defaultNode = null; + + private static ?int $defaultClockSeq = null; + private UuidV1Factory $factory; + /** - * @param int<0, 281474976710655>|non-empty-string|null $node + * @param non-empty-string $field The name of the field to store the UUID + * @param bool $nullable Indicates whether the UUID can be null + * @param int<0, 281474976710655>|non-empty-string|null $node A 48-bit integer or hexadecimal string representing the hardware address + * @param int|null $clockSeq A number used to help avoid duplicates that could arise when the clock is set backwards in time */ public function __construct( - private string $field = 'uuid', - private int|string|null $node = null, - private ?int $clockSeq = null, - private bool $nullable = false, - ) {} - - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void - { - if ($this->nullable || isset($event->state->getData()[$this->field])) { - return; - } + string $field, + bool $nullable = false, + private readonly int|string|null $node = null, + private readonly ?int $clockSeq = null, + ) { + $this->factory = new UuidV1Factory(); + parent::__construct($field, $nullable); + } - $identifier = (new UuidFactory())->v1( - $this->node, - $this->clockSeq, - ); + /** + * Set default node and clock sequence for UUIDv1 generation. + * + * @param int<0, 281474976710655>|non-empty-string|null $node The node to set + * @param int|null $clockSeq The clock sequence to set + */ + public static function setDefaults(int|string|null $node, ?int $clockSeq): void + { + self::$defaultNode = $node; + self::$defaultClockSeq = $clockSeq; + } - $event->state->register($this->field, $identifier); + #[\Override] + protected function createValue(): \Ramsey\Identifier\Uuid + { + $node = $this->node ?? self::$defaultNode; + $clockSeq = $this->clockSeq ?? self::$defaultClockSeq; + return $this->factory->create($node, $clockSeq); } } diff --git a/src/Listener/Uuid2.php b/src/Listener/Uuid2.php index 8d771f6..353c56b 100644 --- a/src/Listener/Uuid2.php +++ b/src/Listener/Uuid2.php @@ -4,42 +4,78 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; -use Cycle\ORM\Entity\Behavior\Attribute\Listen; -use Cycle\ORM\Entity\Behavior\Event\Mapper\Command\OnCreate; use Ramsey\Identifier\Uuid\DceDomain; -use Ramsey\Identifier\Uuid\UuidFactory; +use Ramsey\Identifier\Uuid\UuidV2Factory; -final class Uuid2 +/** + * Generates UUIDv2 (DCE Security) identifiers for entities. + * You can set default values using the {@see setDefaults()} method. + */ +final class Uuid2 extends BaseUuid { + private static DceDomain|int $defaultLocalDomain = DceDomain::Person; + + /** @var int<0, 4294967295>|null */ + private static ?int $defaultLocalIdentifier = null; + + /** @var int<0, 281474976710655>|non-empty-string|null */ + private static int|string|null $defaultNode = null; + + private static ?int $defaultClockSeq = null; + private UuidV2Factory $factory; + /** - * @param int<0, 4294967295>| null $localIdentifier $localIdentifier - * @param int<0, 281474976710655>|non-empty-string|null $node + * @param non-empty-string $field The name of the field to store the UUID + * @param bool $nullable Indicates whether the UUID can be null + * @param DceDomain|int|null $localDomain The local domain to which the local identifier belongs + * @param int<0, 4294967295>|null $localIdentifier A 32-bit local identifier belonging to the local domain + * @param int<0, 281474976710655>|non-empty-string|null $node A 48-bit integer or hexadecimal string representing the hardware address + * @param int|null $clockSeq A number used to help avoid duplicates that could arise when the clock is set backwards in time */ public function __construct( - private string $field = 'uuid', - private DceDomain|int $localDomain = 0, - private ?int $localIdentifier = null, - private int|string|null $node = null, - private ?int $clockSeq = null, - private bool $nullable = false, - ) {} - - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void - { - if ($this->nullable || isset($event->state->getData()[$this->field])) { - return; + string $field, + bool $nullable = false, + private readonly DceDomain|int|null $localDomain = null, + private readonly ?int $localIdentifier = null, + private readonly int|string|null $node = null, + private readonly ?int $clockSeq = null, + ) { + $this->factory = new UuidV2Factory(); + parent::__construct($field, $nullable); + } + + /** + * Set default values for UUIDv2 generation. + * + * @param DceDomain|int|null $localDomain The local domain + * @param int<0, 4294967295>|null $localIdentifier The local identifier + * @param int<0, 281474976710655>|non-empty-string|null $node The node + * @param int|null $clockSeq The clock sequence + */ + public static function setDefaults( + DceDomain|int|null $localDomain, + ?int $localIdentifier, + int|string|null $node, + ?int $clockSeq, + ): void { + if ($localDomain !== null) { + self::$defaultLocalDomain = $localDomain; } + self::$defaultLocalIdentifier = $localIdentifier; + self::$defaultNode = $node; + self::$defaultClockSeq = $clockSeq; + } - $this->localDomain = \is_int($this->localDomain) ? DceDomain::from($this->localDomain) : $this->localDomain; + #[\Override] + protected function createValue(): \Ramsey\Identifier\Uuid + { + $localDomain = $this->localDomain ?? self::$defaultLocalDomain; + $localIdentifier = $this->localIdentifier ?? self::$defaultLocalIdentifier; + $node = $this->node ?? self::$defaultNode; + $clockSeq = $this->clockSeq ?? self::$defaultClockSeq; - $identifier = (new UuidFactory())->v2( - $this->localDomain, - $this->localIdentifier, - $this->node, - $this->clockSeq, - ); + $localDomain = \is_int($localDomain) ? DceDomain::from($localDomain) : $localDomain; - $event->state->register($this->field, $identifier); + return $this->factory->create($localDomain, $localIdentifier, $node, $clockSeq); } } diff --git a/src/Listener/Uuid3.php b/src/Listener/Uuid3.php index 523eb3e..7adb4e1 100644 --- a/src/Listener/Uuid3.php +++ b/src/Listener/Uuid3.php @@ -4,33 +4,37 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; -use Cycle\ORM\Entity\Behavior\Attribute\Listen; -use Cycle\ORM\Entity\Behavior\Event\Mapper\Command\OnCreate; +use Cycle\ORM\Entity\Behavior\Identifier\Listener\BaseUuid as Base; use Ramsey\Identifier\Uuid; use Ramsey\Identifier\Uuid\NamespaceId; -use Ramsey\Identifier\Uuid\UuidFactory; +use Ramsey\Identifier\Uuid\UuidV3Factory; -final class Uuid3 +/** + * Generates UUIDv3 (name-based with MD5 hashing) identifiers for entities. + */ +final class Uuid3 extends Base { + private UuidV3Factory $factory; + + /** + * @param non-empty-string $field The name of the field to store the UUID + * @param NamespaceId|BaseUuid|string $namespace The namespace UUID + * @param string $name The name to hash + * @param bool $nullable Indicates whether the UUID can be null + */ public function __construct( - private NamespaceId|Uuid|string $namespace, - private string $name, - private string $field = 'uuid', - private bool $nullable = false, - ) {} + string $field, + private readonly NamespaceId|Uuid|string $namespace, + private readonly string $name, + bool $nullable = false, + ) { + $this->factory = new UuidV3Factory(); + parent::__construct($field, $nullable); + } - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void + #[\Override] + protected function createValue(): \Ramsey\Identifier\Uuid { - if ($this->nullable || isset($event->state->getData()[$this->field])) { - return; - } - - $identifier = (new UuidFactory())->v3( - $this->namespace, - $this->name, - ); - - $event->state->register($this->field, $identifier); + return $this->factory->create($this->namespace, $this->name); } } diff --git a/src/Listener/Uuid4.php b/src/Listener/Uuid4.php index 5d8771f..fff09eb 100644 --- a/src/Listener/Uuid4.php +++ b/src/Listener/Uuid4.php @@ -4,26 +4,30 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; -use Cycle\ORM\Entity\Behavior\Attribute\Listen; -use Cycle\ORM\Entity\Behavior\Event\Mapper\Command\OnCreate; -use Ramsey\Identifier\Uuid\UuidFactory; +use Ramsey\Identifier\Uuid\UuidV4Factory; -final class Uuid4 +/** + * Generates UUIDv4 (random) identifiers for entities. + */ +final class Uuid4 extends BaseUuid { + private UuidV4Factory $factory; + + /** + * @param non-empty-string $field The name of the field to store the UUID + * @param bool $nullable Indicates whether the UUID can be null + */ public function __construct( - private string $field = 'uuid', - private bool $nullable = false, - ) {} + string $field, + bool $nullable = false, + ) { + $this->factory = new UuidV4Factory(); + parent::__construct($field, $nullable); + } - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void + #[\Override] + protected function createValue(): \Ramsey\Identifier\Uuid { - if ($this->nullable || isset($event->state->getData()[$this->field])) { - return; - } - - $identifier = (new UuidFactory())->v4(); - - $event->state->register($this->field, $identifier); + return $this->factory->create(); } } diff --git a/src/Listener/Uuid5.php b/src/Listener/Uuid5.php index 1fc8df3..e14a6dd 100644 --- a/src/Listener/Uuid5.php +++ b/src/Listener/Uuid5.php @@ -4,33 +4,37 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; -use Cycle\ORM\Entity\Behavior\Attribute\Listen; -use Cycle\ORM\Entity\Behavior\Event\Mapper\Command\OnCreate; +use Cycle\ORM\Entity\Behavior\Identifier\Listener\BaseUuid as Base; use Ramsey\Identifier\Uuid; use Ramsey\Identifier\Uuid\NamespaceId; -use Ramsey\Identifier\Uuid\UuidFactory; +use Ramsey\Identifier\Uuid\UuidV5Factory; -final class Uuid5 +/** + * Generates UUIDv5 (name-based with SHA-1 hashing) identifiers for entities. + */ +final class Uuid5 extends Base { + private UuidV5Factory $factory; + + /** + * @param non-empty-string $field The name of the field to store the UUID + * @param NamespaceId|BaseUuid|string $namespace The namespace UUID + * @param string $name The name to hash + * @param bool $nullable Indicates whether the UUID can be null + */ public function __construct( - private NamespaceId|Uuid|string $namespace, - private string $name, - private string $field = 'uuid', - private bool $nullable = false, - ) {} + string $field, + private readonly NamespaceId|Uuid|string $namespace, + private readonly string $name, + bool $nullable = false, + ) { + $this->factory = new UuidV5Factory(); + parent::__construct($field, $nullable); + } - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void + #[\Override] + protected function createValue(): \Ramsey\Identifier\Uuid { - if ($this->nullable || isset($event->state->getData()[$this->field])) { - return; - } - - $identifier = (new UuidFactory())->v5( - $this->namespace, - $this->name, - ); - - $event->state->register($this->field, $identifier); + return $this->factory->create($this->namespace, $this->name); } } diff --git a/src/Listener/Uuid6.php b/src/Listener/Uuid6.php index c60f2b1..a39a2cf 100644 --- a/src/Listener/Uuid6.php +++ b/src/Listener/Uuid6.php @@ -4,37 +4,55 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; -use Cycle\ORM\Entity\Behavior\Attribute\Listen; -use Cycle\ORM\Entity\Behavior\Event\Mapper\Command\OnCreate; use Ramsey\Identifier\Service\Nic\Nic; -use Ramsey\Identifier\Uuid\UuidFactory; +use Ramsey\Identifier\Uuid\UuidV6Factory; -final class Uuid6 +/** + * Generates UUIDv6 (ordered-time) identifiers for entities. + * You can set default node and clock sequence using the {@see setDefaults()} method. + */ +final class Uuid6 extends BaseUuid { + /** @var int<0, 281474976710655>|non-empty-string|null */ + private static int|string|null $defaultNode = null; + + private static ?int $defaultClockSeq = null; + private UuidV6Factory $factory; + /** - * @param int<0, 281474976710655>|non-empty-string|null $node + * @param non-empty-string $field The name of the field to store the UUID + * @param bool $nullable Indicates whether the UUID can be null + * @param int<0, 281474976710655>|non-empty-string|null $node A 48-bit integer or hexadecimal string representing the hardware address + * @param int|null $clockSeq A number used to help avoid duplicates that could arise when the clock is set backwards in time */ public function __construct( - private string $field = 'uuid', - private int|string|null $node = null, - private ?int $clockSeq = null, - private bool $nullable = false, - ) {} - - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void - { - if ($this->nullable || isset($event->state->getData()[$this->field])) { - return; - } - - $this->node = $this->node instanceof Nic ? $this->node->address() : $this->node; + string $field, + bool $nullable = false, + private readonly int|string|null $node = null, + private readonly ?int $clockSeq = null, + ) { + $this->factory = new UuidV6Factory(); + parent::__construct($field, $nullable); + } - $identifier = (new UuidFactory())->v6( - $this->node, - $this->clockSeq, - ); + /** + * Set default node and clock sequence for UUIDv6 generation. + * + * @param int<0, 281474976710655>|non-empty-string|null $node The node to set + * @param int|null $clockSeq The clock sequence to set + */ + public static function setDefaults(int|string|null $node, ?int $clockSeq): void + { + self::$defaultNode = $node; + self::$defaultClockSeq = $clockSeq; + } - $event->state->register($this->field, $identifier); + #[\Override] + protected function createValue(): \Ramsey\Identifier\Uuid + { + $node = $this->node ?? self::$defaultNode; + $clockSeq = $this->clockSeq ?? self::$defaultClockSeq; + $node = $node instanceof Nic ? $node->address() : $node; + return $this->factory->create($node, $clockSeq); } } diff --git a/src/Listener/Uuid7.php b/src/Listener/Uuid7.php index d71cf7d..522e5df 100644 --- a/src/Listener/Uuid7.php +++ b/src/Listener/Uuid7.php @@ -4,26 +4,30 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; -use Cycle\ORM\Entity\Behavior\Attribute\Listen; -use Cycle\ORM\Entity\Behavior\Event\Mapper\Command\OnCreate; -use Ramsey\Identifier\Uuid\UuidFactory; +use Ramsey\Identifier\Uuid\UuidV7Factory; -final class Uuid7 +/** + * Generates UUIDv7 (time-ordered with random data) identifiers for entities. + */ +final class Uuid7 extends BaseUuid { + private UuidV7Factory $factory; + + /** + * @param non-empty-string $field The name of the field to store the UUID + * @param bool $nullable Indicates whether the UUID can be null + */ public function __construct( - private string $field = 'uuid', - private bool $nullable = false, - ) {} + string $field, + bool $nullable = false, + ) { + $this->factory = new UuidV7Factory(); + parent::__construct($field, $nullable); + } - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void + #[\Override] + protected function createValue(): \Ramsey\Identifier\Uuid { - if ($this->nullable || isset($event->state->getData()[$this->field])) { - return; - } - - $identifier = (new UuidFactory())->v7(); - - $event->state->register($this->field, $identifier); + return $this->factory->create(); } } diff --git a/src/Snowflake.php b/src/Snowflake.php new file mode 100644 index 0000000..a360c7c --- /dev/null +++ b/src/Snowflake.php @@ -0,0 +1,66 @@ +role); + $this->column = $modifier->findColumnName($this->field, $this->column); + if (\is_string($this->column) && $this->column !== '') { + $modifier->addSnowflakeColumn( + $this->column, + $this->field, + $this->nullable ? null : GeneratedField::BEFORE_INSERT, + )->nullable($this->nullable); + + $factory = $this->snowflakeFactory(); + + $modifier->setTypecast( + $registry->getEntity($this->role)->getFields()->get($this->field), + [$factory, 'createFromInteger'], + ); + } + } + + #[\Override] + public function render(Registry $registry): void + { + $modifier = new RegistryModifier($registry, $this->role); + /** @var non-empty-string column */ + $this->column = $modifier->findColumnName($this->field, $this->column) ?? $this->field; + + $modifier->addSnowflakeColumn( + $this->column, + $this->field, + $this->nullable ? null : GeneratedField::BEFORE_INSERT, + )->nullable($this->nullable); + + $factory = $this->snowflakeFactory(); + + $modifier->setTypecast( + $registry->getEntity($this->role)->getFields()->get($this->field), + [$factory, 'createFromInteger'], + ); + } + + abstract protected function snowflakeFactory(): SnowflakeFactory; +} diff --git a/src/SnowflakeDiscord.php b/src/SnowflakeDiscord.php new file mode 100644 index 0000000..bba5f6e --- /dev/null +++ b/src/SnowflakeDiscord.php @@ -0,0 +1,74 @@ +|null $workerId A worker identifier to use when creating Snowflakes + * @param int<0, 281474976710655>|null $processId A process identifier to use when creating Snowflakes + * @param bool $nullable Indicates whether to generate a new Snowflake or not + * + * @see \Ramsey\Identifier\Snowflake\DiscordSnowflakeFactory::create() + */ + public function __construct( + string $field, + ?string $column = null, + private readonly ?int $workerId = null, + private readonly ?int $processId = null, + bool $nullable = false, + ) { + $this->field = $field; + $this->column = $column; + $this->nullable = $nullable; + } + + #[\Override] + protected function getListenerClass(): string + { + return Listener::class; + } + + /** + * @return array{ + * field: non-empty-string, + * workerId: null|int<0, 281474976710655>, + * processId: null|int<0, 281474976710655>, + * nullable: bool + * } + */ + #[\Override] + protected function getListenerArgs(): array + { + return [ + 'field' => $this->field, + 'workerId' => $this->workerId, + 'processId' => $this->processId, + 'nullable' => $this->nullable, + ]; + } + + #[\Override] + protected function snowflakeFactory(): SnowflakeFactory + { + return new DiscordSnowflakeFactory($this->workerId, $this->processId); + } +} diff --git a/src/SnowflakeGeneric.php b/src/SnowflakeGeneric.php new file mode 100644 index 0000000..eca47d7 --- /dev/null +++ b/src/SnowflakeGeneric.php @@ -0,0 +1,76 @@ +|null $node A node identifier to use when creating Snowflakes + * @param Epoch|int|null $epochOffset The offset from the Unix Epoch in milliseconds + * @param bool $nullable Indicates whether to generate a new Snowflake or not + * + * @see \Ramsey\Identifier\Snowflake\GenericSnowflakeFactory::create() + */ + public function __construct( + string $field, + ?string $column = null, + private readonly ?int $node = null, + private readonly Epoch|int|null $epochOffset = null, + bool $nullable = false, + ) { + $this->field = $field; + $this->column = $column; + $this->nullable = $nullable; + } + + #[\Override] + protected function getListenerClass(): string + { + return Listener::class; + } + + /** + * @return array{ + * field: non-empty-string, + * node: null|int<0, 1023>, + * epochOffset: Epoch|int|null, + * nullable: bool + * } + */ + #[\Override] + protected function getListenerArgs(): array + { + return [ + 'field' => $this->field, + 'node' => $this->node, + 'epochOffset' => $this->epochOffset, + 'nullable' => $this->nullable, + ]; + } + + #[\Override] + protected function snowflakeFactory(): SnowflakeFactory + { + return new GenericSnowflakeFactory($this->node, $this->epochOffset); + } +} diff --git a/src/SnowflakeInstagram.php b/src/SnowflakeInstagram.php new file mode 100644 index 0000000..cc08026 --- /dev/null +++ b/src/SnowflakeInstagram.php @@ -0,0 +1,70 @@ +|null $shardId A shard identifier to use when creating Snowflakes + * @param bool $nullable Indicates whether to generate a new Snowflake or not + * + * @see \Ramsey\Identifier\Snowflake\InstagramSnowflakeFactory::create() + */ + public function __construct( + string $field, + ?string $column = null, + private readonly ?int $shardId = null, + bool $nullable = false, + ) { + $this->field = $field; + $this->column = $column; + $this->nullable = $nullable; + } + + #[\Override] + protected function getListenerClass(): string + { + return Listener::class; + } + + /** + * @return array{ + * field: non-empty-string, + * shardId: null|int<0, 1023>, + * nullable: bool + * } + */ + #[\Override] + protected function getListenerArgs(): array + { + return [ + 'field' => $this->field, + 'shardId' => $this->shardId, + 'nullable' => $this->nullable, + ]; + } + + #[\Override] + protected function snowflakeFactory(): SnowflakeFactory + { + return new InstagramSnowflakeFactory($this->shardId); + } +} diff --git a/src/SnowflakeMastodon.php b/src/SnowflakeMastodon.php new file mode 100644 index 0000000..6abf0c8 --- /dev/null +++ b/src/SnowflakeMastodon.php @@ -0,0 +1,70 @@ +field = $field; + $this->column = $column; + $this->nullable = $nullable; + } + + #[\Override] + protected function getListenerClass(): string + { + return Listener::class; + } + + /** + * @return array{ + * field: non-empty-string, + * tableName: non-empty-string|null, + * nullable: bool + * } + */ + #[\Override] + protected function getListenerArgs(): array + { + return [ + 'field' => $this->field, + 'tableName' => $this->tableName, + 'nullable' => $this->nullable, + ]; + } + + #[\Override] + protected function snowflakeFactory(): SnowflakeFactory + { + return new MastodonSnowflakeFactory($this->tableName); + } +} diff --git a/src/SnowflakeTwitter.php b/src/SnowflakeTwitter.php new file mode 100644 index 0000000..f079464 --- /dev/null +++ b/src/SnowflakeTwitter.php @@ -0,0 +1,70 @@ +|null $machineId A machine identifier to use when creating Snowflakes + * @param bool $nullable Indicates whether to generate a new Snowflake or not + * + * @see \Ramsey\Identifier\Snowflake\TwitterSnowflakeFactory::create() + */ + public function __construct( + string $field, + ?string $column = null, + private readonly ?int $machineId = null, + bool $nullable = false, + ) { + $this->field = $field; + $this->column = $column; + $this->nullable = $nullable; + } + + #[\Override] + protected function getListenerClass(): string + { + return Listener::class; + } + + /** + * @return array{ + * field: non-empty-string, + * machineId: null|int<0, 1023>, + * nullable: bool + * } + */ + #[\Override] + protected function getListenerArgs(): array + { + return [ + 'field' => $this->field, + 'machineId' => $this->machineId, + 'nullable' => $this->nullable, + ]; + } + + #[\Override] + protected function snowflakeFactory(): SnowflakeFactory + { + return new TwitterSnowflakeFactory($this->machineId); + } +} diff --git a/src/Ulid.php b/src/Ulid.php index 55b0c13..d54761c 100644 --- a/src/Ulid.php +++ b/src/Ulid.php @@ -52,7 +52,7 @@ public function compute(Registry $registry): void { $modifier = new RegistryModifier($registry, $this->role); $this->column = $modifier->findColumnName($this->field, $this->column); - if ($this->column !== null) { + if (\is_string($this->column) && $this->column !== '') { $modifier->addUlidColumn( $this->column, $this->field, @@ -70,6 +70,7 @@ public function compute(Registry $registry): void public function render(Registry $registry): void { $modifier = new RegistryModifier($registry, $this->role); + /** @var non-empty-string column */ $this->column = $modifier->findColumnName($this->field, $this->column) ?? $this->field; $modifier->addUlidColumn( diff --git a/src/Uuid.php b/src/Uuid.php index 8bc81d9..9997329 100644 --- a/src/Uuid.php +++ b/src/Uuid.php @@ -30,7 +30,7 @@ public function compute(Registry $registry): void { $modifier = new RegistryModifier($registry, $this->role); $this->column = $modifier->findColumnName($this->field, $this->column); - if ($this->column !== null) { + if (\is_string($this->column) && $this->column !== '') { $modifier->addUuidColumn( $this->column, $this->field, @@ -48,6 +48,7 @@ public function compute(Registry $registry): void public function render(Registry $registry): void { $modifier = new RegistryModifier($registry, $this->role); + /** @var non-empty-string column */ $this->column = $modifier->findColumnName($this->field, $this->column) ?? $this->field; $modifier->addUuidColumn( diff --git a/src/Uuid1.php b/src/Uuid1.php index efcb70e..0afae6a 100644 --- a/src/Uuid1.php +++ b/src/Uuid1.php @@ -21,6 +21,13 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class Uuid1 extends BaseUuid { + /** + * @var Nic|int<0, 281474976710655>|non-empty-string|null $node + */ + private Nic|int|string|null $node = null; + + private ?int $clockSeq = null; + /** * @param non-empty-string $field Uuid property name * @param non-empty-string|null $column Uuid column name @@ -36,13 +43,15 @@ final class Uuid1 extends BaseUuid public function __construct( string $field = 'uuid', ?string $column = null, - private Nic|int|string|null $node = null, - private ?int $clockSeq = null, + Nic|int|string|null $node = null, + ?int $clockSeq = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; + $this->node = $node; + $this->clockSeq = $clockSeq; } #[\Override] diff --git a/src/Uuid2.php b/src/Uuid2.php index c3e1868..424be17 100644 --- a/src/Uuid2.php +++ b/src/Uuid2.php @@ -22,10 +22,20 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class Uuid2 extends BaseUuid { + private DceDomain|int $localDomain; + private ?int $localIdentifier; + + /** + * @var Nic|int<0, 281474976710655>|non-empty-string|null $node + */ + private Nic|int|string|null $node; + + private ?int $clockSeq; + /** * @param non-empty-string $field Uuid property name * @param non-empty-string|null $column Uuid column name - * @param DceDomain|int $localDomain The local domain to which the local identifier belongs; this defaults to "Person" + * @param DceDomain|int|null $localDomain The local domain to which the local identifier belongs; this defaults to "Person" * and if $localIdentifier is not provided, the factory will attempt to get a suitable local ID for the domain * (e.g., the UID or GID of the user running the script). * @param int<0, 4294967295> | null $localIdentifier A 32-bit local identifier belonging to the local domain @@ -43,15 +53,19 @@ final class Uuid2 extends BaseUuid public function __construct( string $field = 'uuid', ?string $column = null, - private DceDomain|int $localDomain = 0, - private ?int $localIdentifier = null, - private Nic|int|string|null $node = null, - private ?int $clockSeq = null, + DceDomain|int|null $localDomain = null, + ?int $localIdentifier = null, + Nic|int|string|null $node = null, + ?int $clockSeq = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; + $this->localDomain = $localDomain ?? DceDomain::Person; + $this->localIdentifier = $localIdentifier; + $this->node = $node; + $this->clockSeq = $clockSeq; } #[\Override] diff --git a/src/Uuid6.php b/src/Uuid6.php index 92d83c5..ce71544 100644 --- a/src/Uuid6.php +++ b/src/Uuid6.php @@ -21,6 +21,13 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class Uuid6 extends BaseUuid { + /** + * @var Nic|int<0, 281474976710655>|non-empty-string|null $node + */ + private Nic|int|string|null $node = null; + + private ?int $clockSeq = null; + /** * @param non-empty-string $field Uuid property name * @param non-empty-string|null $column Uuid column name @@ -36,13 +43,15 @@ final class Uuid6 extends BaseUuid public function __construct( string $field = 'uuid', ?string $column = null, - private Nic|int|string|null $node = null, - private ?int $clockSeq = null, + Nic|int|string|null $node = null, + ?int $clockSeq = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; + $this->node = $node; + $this->clockSeq = $clockSeq; } #[\Override] diff --git a/tests/Identifier/Fixtures/Combined/MultipleIdentifiers.php b/tests/Identifier/Fixtures/Combined/MultipleIdentifiers.php index 08193fe..ad70b03 100644 --- a/tests/Identifier/Fixtures/Combined/MultipleIdentifiers.php +++ b/tests/Identifier/Fixtures/Combined/MultipleIdentifiers.php @@ -6,23 +6,27 @@ use Cycle\Annotated\Annotation\Column; use Cycle\Annotated\Annotation\Entity; -use Cycle\ORM\Entity\Behavior\Identifier\Ulid; -use Cycle\ORM\Entity\Behavior\Identifier\Uuid4; -use Ramsey\Identifier\Ulid as UlidInterface; +use Cycle\ORM\Entity\Behavior\Identifier; +use Ramsey\Identifier\Snowflake; +use Ramsey\Identifier\Ulid; use Ramsey\Identifier\Uuid; /** * @Entity - * @Uuid4 - * @Uuid4(field="uuidNullable", column="uuid_nullable", nullable=true) - * @Ulid(field="ulid") - * @Ulid(field="ulidNullable", column="ulid_nullable", nullable=true) + * @Identifier\Uuid4 + * @Identifier\Uuid4(field="uuidNullable", column="uuid_nullable", nullable=true) + * @Identifier\Ulid(field="ulid") + * @Identifier\Ulid(field="ulidNullable", column="ulid_nullable", nullable=true) + * @Identifier\SnowflakeGeneric(field="snowflake") + * @Identifier\SnowflakeGeneric(field="snowflakeNullable", column="snowflake_nullable", nullable=true) */ #[Entity] -#[Uuid4] -#[Uuid4(field: 'uuidNullable', column: 'uuid_nullable', nullable: true)] -#[Ulid(field: 'ulid')] -#[Uuid4(field: 'ulidNullable', column: 'ulid_nullable', nullable: true)] +#[Identifier\Uuid4] +#[Identifier\Uuid4(field: 'uuidNullable', column: 'uuid_nullable', nullable: true)] +#[Identifier\Ulid(field: 'ulid')] +#[Identifier\Uuid4(field: 'ulidNullable', column: 'ulid_nullable', nullable: true)] +#[Identifier\SnowflakeGeneric(field: 'snowflake')] +#[Identifier\SnowflakeGeneric(field: 'snowflakeNullable', column: 'snowflake_nullable', nullable: true)] class MultipleIdentifiers { /** @@ -41,11 +45,23 @@ class MultipleIdentifiers * @Column(type="ulid") */ #[Column(type: 'ulid')] - public UlidInterface $ulid; + public Ulid $ulid; /** * @Column(type="ulid", nullable=true) */ #[Column(type: 'ulid')] - public ?UlidInterface $ulidNullable = null; + public ?Ulid $ulidNullable = null; + + /** + * @Column(type="snowflake") + */ + #[Column(type: 'snowflake')] + public Snowflake $snowflake; + + /** + * @Column(type="snowflake", nullable=true) + */ + #[Column(type: 'snowflake')] + public ?Snowflake $snowflakeNullable = null; } diff --git a/tests/Identifier/Fixtures/Snowflake/MultipleSnowflake.php b/tests/Identifier/Fixtures/Snowflake/MultipleSnowflake.php new file mode 100644 index 0000000..e57bb84 --- /dev/null +++ b/tests/Identifier/Fixtures/Snowflake/MultipleSnowflake.php @@ -0,0 +1,57 @@ +assertSame(GeneratedField::BEFORE_INSERT, $fields->get('ulidNullable')->getGenerated()); } + #[\Override] public function setUp(): void { parent::setUp(); diff --git a/tests/Identifier/Functional/Driver/Common/Combined/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Combined/ListenerTest.php index 95322c3..203ecc2 100644 --- a/tests/Identifier/Functional/Driver/Common/Combined/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Combined/ListenerTest.php @@ -7,6 +7,7 @@ use Cycle\ORM\Entity\Behavior\Identifier\Tests\Fixtures\Combined\MultipleIdentifiers; use Cycle\ORM\Entity\Behavior\Identifier\Tests\Functional\Driver\Common\BaseTest; use Cycle\ORM\Entity\Behavior\Identifier\Tests\Traits\TableTrait; +use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeGeneric as SnowflakeGenericListener; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Ulid as UlidListener; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid4 as Uuid4Listener; use Cycle\ORM\Entity\Behavior\Identifier\Ulid; @@ -15,6 +16,8 @@ use Cycle\ORM\Schema; use Cycle\ORM\SchemaInterface; use Cycle\ORM\Select; +use Ramsey\Identifier\Snowflake\GenericSnowflakeFactory; +use Ramsey\Identifier\Snowflake as SnowflakeInterface; use Ramsey\Identifier\Ulid as UlidInterface; use Ramsey\Identifier\Ulid\UlidFactory; use Ramsey\Identifier\Uuid\UntypedUuid; @@ -29,13 +32,16 @@ public function testAssignManually(): void $this->withListeners([ Uuid4Listener::class, UlidListener::class, + SnowflakeGenericListener::class, ]); $identifiers = new MultipleIdentifiers(); $identifiers->uuid = (new UuidFactory())->v4(); $identifiers->ulid = (new UlidFactory())->create(); + $identifiers->snowflake = (new GenericSnowflakeFactory(0, 0))->create(); $uuidBytes = $identifiers->uuid->toBytes(); $ulidBytes = $identifiers->ulid->toBytes(); + $snowflakeBytes = $identifiers->snowflake->toBytes(); $this->save($identifiers); @@ -44,6 +50,7 @@ public function testAssignManually(): void $this->assertSame($uuidBytes, $data->uuid->toBytes()); $this->assertSame($ulidBytes, $data->ulid->toBytes()); + $this->assertSame($snowflakeBytes, $data->snowflake->toBytes()); } public function testWithNullableTrue(): void @@ -51,11 +58,13 @@ public function testWithNullableTrue(): void $this->withListeners([ Uuid4Listener::class, UlidListener::class, + SnowflakeGenericListener::class, ]); $identifiers = new MultipleIdentifiers(); $identifiers->uuid = (new UuidFactory())->v4(); $identifiers->ulid = (new UlidFactory())->create(); + $identifiers->snowflake = (new GenericSnowflakeFactory(0, 0))->create(); $this->save($identifiers); @@ -64,6 +73,7 @@ public function testWithNullableTrue(): void $this->assertNull($data[0]['uuid_nullable']); $this->assertNull($data[0]['ulid_nullable']); + $this->assertNull($data[0]['snowflake_nullable']); } public function testCombined(): void @@ -71,6 +81,7 @@ public function testCombined(): void $this->withListeners([ Uuid4Listener::class, UlidListener::class, + SnowflakeGenericListener::class, ]); $identifiers = new MultipleIdentifiers(); @@ -81,12 +92,16 @@ public function testCombined(): void $this->assertInstanceOf(UntypedUuid::class, $data->uuid); $this->assertInstanceOf(UlidInterface::class, $data->ulid); + $this->assertInstanceOf(SnowflakeInterface::class, $data->snowflake); $this->assertNull($data->uuidNullable); $this->assertNull($data->ulidNullable); + $this->assertNull($data->snowflakeNullable); $this->assertIsString($data->uuid->toBytes()); $this->assertIsString($data->uuid->toString()); $this->assertIsString($data->ulid->toBytes()); $this->assertIsString($data->ulid->toString()); + $this->assertIsString($data->snowflake->toBytes()); + $this->assertIsString($data->snowflake->toString()); } public function testComparison(): void @@ -94,6 +109,7 @@ public function testComparison(): void $this->withListeners([ Uuid4Listener::class, UlidListener::class, + SnowflakeGenericListener::class, ]); $expectedDate = '2025-06-17 03:24:36.160 +00:00'; @@ -101,6 +117,7 @@ public function testComparison(): void $identifiers = new MultipleIdentifiers(); $identifiers->uuid = (new UuidFactory())->createFromString('01977bea-d1c0-7154-87bb-6550974155c2'); $identifiers->ulid = (new UlidFactory())->createFromString('01JXXYNME0E5A8FEV5A2BM2NE2'); + $identifiers->snowflake = (new GenericSnowflakeFactory(0, 0))->createFromInteger(7340580095540599922); $this->save($identifiers); @@ -109,18 +126,29 @@ public function testComparison(): void $this->assertSame($expectedDate, $data->uuid->getDateTime()->format('Y-m-d H:i:s.v P')); $this->assertSame($expectedDate, $data->ulid->getDateTime()->format('Y-m-d H:i:s.v P')); + $this->assertSame($expectedDate, $data->snowflake->getDateTime()->format('Y-m-d H:i:s.v P')); $this->assertTrue($data->uuid->equals($data->ulid)); + $this->assertFalse($data->uuid->equals($data->snowflake)); } public function withListeners(array|string $listeners): void { + $factory = new GenericSnowflakeFactory(0, 0); + $this->withSchema(new Schema([ MultipleIdentifiers::class => [ SchemaInterface::ROLE => 'multiple_identifier', SchemaInterface::DATABASE => 'default', SchemaInterface::TABLE => 'multiple_identifiers', SchemaInterface::PRIMARY_KEY => 'ulid', - SchemaInterface::COLUMNS => ['uuid', 'uuid_nullable', 'ulid', 'ulid_nullable'], + SchemaInterface::COLUMNS => [ + 'uuid', + 'uuid_nullable', + 'ulid', + 'ulid_nullable', + 'snowflake', + 'snowflake_nullable', + ], SchemaInterface::LISTENERS => [$listeners], SchemaInterface::SCHEMA => [], SchemaInterface::RELATIONS => [], @@ -129,11 +157,14 @@ public function withListeners(array|string $listeners): void 'uuid_nullable' => [Uuid::class, 'fromString'], 'ulid' => [Ulid::class, 'fromString'], 'ulid_nullable' => [Ulid::class, 'fromString'], + 'snowflake' => [$factory, 'createFromInteger'], + 'snowflake_nullable' => [$factory, 'createFromInteger'], ], ], ])); } + #[\Override] public function setUp(): void { parent::setUp(); @@ -145,6 +176,8 @@ public function setUp(): void 'uuid_nullable' => 'string,nullable', 'ulid' => 'string', 'ulid_nullable' => 'string,nullable', + 'snowflake' => 'snowflake', + 'snowflake_nullable' => 'snowflake,nullable', ], ); } diff --git a/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php new file mode 100644 index 0000000..856f684 --- /dev/null +++ b/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php @@ -0,0 +1,288 @@ +withListeners(SnowflakeGenericListener::class); + + $user = new User(); + $user->snowflake = (new GenericSnowflakeFactory(0, 0))->create(); + $bytes = $user->snowflake->toBytes(); + + $this->save($user); + + $select = new Select($this->orm->with(heap: new Heap()), User::class); + $data = $select->fetchOne(); + + $this->assertSame($bytes, $data->snowflake->toBytes()); + } + + public function testDiscordSnowflake(): void + { + $this->withListeners([ + SnowflakeDiscordListener::class, + [ + 'workerId' => 10, + 'processId' => 20, + ], + ]); + + $user = new User(); + $this->save($user); + + $select = new Select($this->orm->with(heap: new Heap()), User::class); + $data = $select->fetchOne(); + + $this->assertInstanceOf(SnowflakeInterface::class, $data->snowflake); + $this->assertIsString($data->snowflake->toBytes()); + $this->assertIsString($data->snowflake->toString()); + } + + public function testNullableDiscordSnowflake(): void + { + $this->withListeners([ + SnowflakeDiscordListener::class, + [ + 'field' => 'foo_snowflake', + 'nullable' => true, + ], + ]); + + $user = new User(); + $user->snowflake = (new DiscordSnowflakeFactory(0, 0))->create(); + + $this->save($user); + + $select = new Select($this->orm->with(heap: new Heap()), User::class); + $data = $select->fetchData(); + + $this->assertNull($data[0]['foo_snowflake']); + } + + public function testGenericSnowflake(): void + { + $this->withListeners([ + SnowflakeGenericListener::class, + [ + 'node' => 10, + 'epochOffset' => 1662744255000, + ], + ]); + + $user = new User(); + $this->save($user); + + $select = new Select($this->orm->with(heap: new Heap()), User::class); + $data = $select->fetchOne(); + + $this->assertInstanceOf(SnowflakeInterface::class, $data->snowflake); + $this->assertIsString($data->snowflake->toBytes()); + $this->assertIsString($data->snowflake->toString()); + } + + public function testNullableGenericSnowflake(): void + { + $this->withListeners([ + SnowflakeGenericListener::class, + [ + 'field' => 'foo_snowflake', + 'nullable' => true, + ], + ]); + + $user = new User(); + $user->snowflake = (new GenericSnowflakeFactory(0, 0))->create(); + + $this->save($user); + + $select = new Select($this->orm->with(heap: new Heap()), User::class); + $data = $select->fetchData(); + + $this->assertNull($data[0]['foo_snowflake']); + } + + public function testInstagramSnowflake(): void + { + $this->withListeners([ + SnowflakeInstagramListener::class, + [ + 'shardId' => 10, + ], + ]); + + $user = new User(); + $this->save($user); + + $select = new Select($this->orm->with(heap: new Heap()), User::class); + $data = $select->fetchOne(); + + $this->assertInstanceOf(SnowflakeInterface::class, $data->snowflake); + $this->assertIsString($data->snowflake->toBytes()); + $this->assertIsString($data->snowflake->toString()); + } + + public function testNullableInstagramSnowflake(): void + { + $this->withListeners([ + SnowflakeInstagramListener::class, + [ + 'field' => 'foo_snowflake', + 'nullable' => true, + ], + ]); + + $user = new User(); + $user->snowflake = (new InstagramSnowflakeFactory(0))->create(); + + $this->save($user); + + $select = new Select($this->orm->with(heap: new Heap()), User::class); + $data = $select->fetchData(); + + $this->assertNull($data[0]['foo_snowflake']); + } + + public function testMastodonSnowflake(): void + { + $this->withListeners([ + SnowflakeMastodonListener::class, + [ + 'tableName' => 'users', + ], + ]); + + $user = new User(); + $this->save($user); + + $select = new Select($this->orm->with(heap: new Heap()), User::class); + $data = $select->fetchOne(); + + $this->assertInstanceOf(SnowflakeInterface::class, $data->snowflake); + $this->assertIsString($data->snowflake->toBytes()); + $this->assertIsString($data->snowflake->toString()); + } + + public function testNullableMastodonSnowflake(): void + { + $this->withListeners([ + SnowflakeMastodonListener::class, + [ + 'field' => 'foo_snowflake', + 'nullable' => true, + ], + ]); + + $user = new User(); + $user->snowflake = (new MastodonSnowflakeFactory(null))->create(); + + $this->save($user); + + $select = new Select($this->orm->with(heap: new Heap()), User::class); + $data = $select->fetchData(); + + $this->assertNull($data[0]['foo_snowflake']); + } + + public function testTwitterSnowflake(): void + { + $this->withListeners([ + SnowflakeTwitterListener::class, + [ + 'machineId' => 10, + ], + ]); + + $user = new User(); + $this->save($user); + + $select = new Select($this->orm->with(heap: new Heap()), User::class); + $data = $select->fetchOne(); + + $this->assertInstanceOf(SnowflakeInterface::class, $data->snowflake); + $this->assertIsString($data->snowflake->toBytes()); + $this->assertIsString($data->snowflake->toString()); + } + + public function testNullableTwitterSnowflake(): void + { + $this->withListeners([ + SnowflakeTwitterListener::class, + [ + 'field' => 'foo_snowflake', + 'nullable' => true, + ], + ]); + + $user = new User(); + $user->snowflake = (new TwitterSnowflakeFactory(0))->create(); + + $this->save($user); + + $select = new Select($this->orm->with(heap: new Heap()), User::class); + $data = $select->fetchData(); + + $this->assertNull($data[0]['foo_snowflake']); + } + + public function withListeners(array|string $listeners): void + { + $factory = new GenericSnowflakeFactory(0, 0); + + $this->withSchema(new Schema([ + User::class => [ + SchemaInterface::ROLE => 'user', + SchemaInterface::DATABASE => 'default', + SchemaInterface::TABLE => 'users', + SchemaInterface::PRIMARY_KEY => 'snowflake', + SchemaInterface::COLUMNS => ['snowflake', 'foo_snowflake'], + SchemaInterface::LISTENERS => [$listeners], + SchemaInterface::SCHEMA => [], + SchemaInterface::RELATIONS => [], + SchemaInterface::TYPECAST => [ + 'snowflake' => [$factory, 'createFromInteger'], + 'foo_snowflake' => [$factory, 'createFromInteger'], + ], + ], + ])); + } + + #[\Override] + public function setUp(): void + { + parent::setUp(); + + $this->makeTable( + 'users', + [ + 'snowflake' => 'snowflake', + 'foo_snowflake' => 'snowflake,nullable', + ], + ); + } +} diff --git a/tests/Identifier/Functional/Driver/Common/Snowflake/SnowflakeTest.php b/tests/Identifier/Functional/Driver/Common/Snowflake/SnowflakeTest.php new file mode 100644 index 0000000..c5e2103 --- /dev/null +++ b/tests/Identifier/Functional/Driver/Common/Snowflake/SnowflakeTest.php @@ -0,0 +1,139 @@ +compileWithTokenizer($this->tokenizer, $reader); + + $fields = $this->registry->getEntity(User::class)->getFields(); + + $this->assertTrue($fields->has('snowflake')); + $this->assertTrue($fields->hasColumn('snowflake')); + $this->assertSame('snowflake', $fields->get('snowflake')->getType()); + $this->assertIsArray($fields->get('snowflake')->getTypecast()); + $this->assertInstanceOf(SnowflakeFactory::class, $fields->get('snowflake')->getTypecast()[0]); + $this->assertSame('createFromInteger', $fields->get('snowflake')->getTypecast()[1]); + $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('snowflake')->getGenerated()); + $this->assertSame(1, $fields->count()); + } + + /** + * @dataProvider readersDataProvider + */ + public function testAddColumn(ReaderInterface $reader): void + { + $this->compileWithTokenizer($this->tokenizer, $reader); + + $fields = $this->registry->getEntity(Post::class)->getFields(); + + $this->assertTrue($fields->has('customSnowflake')); + $this->assertTrue($fields->hasColumn('custom_snowflake')); + $this->assertSame('snowflake', $fields->get('customSnowflake')->getType()); + $this->assertInstanceOf(SnowflakeFactory::class, $fields->get('customSnowflake')->getTypecast()[0] ?? null); + $this->assertSame('createFromInteger', $fields->get('customSnowflake')->getTypecast()[1] ?? null); + $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('customSnowflake')->getGenerated()); + } + + /** + * @dataProvider readersDataProvider + */ + public function testMultipleSnowflake(ReaderInterface $reader): void + { + $this->compileWithTokenizer($this->tokenizer, $reader); + + $fields = $this->registry->getEntity(MultipleSnowflake::class)->getFields(); + + $this->assertTrue($fields->has('snowflake')); + $this->assertTrue($fields->hasColumn('snowflake')); + $this->assertSame('snowflake', $fields->get('snowflake')->getType()); + $this->assertInstanceOf(SnowflakeFactory::class, $fields->get('snowflake')->getTypecast()[0] ?? null); + $this->assertSame('createFromInteger', $fields->get('snowflake')->getTypecast()[1] ?? null); + $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('snowflake')->getGenerated()); + + $this->assertTrue($fields->has('discord')); + $this->assertTrue($fields->hasColumn('discord')); + $this->assertSame('snowflake', $fields->get('discord')->getType()); + $this->assertInstanceOf(SnowflakeFactory::class, $fields->get('discord')->getTypecast()[0] ?? null); + $this->assertSame('createFromInteger', $fields->get('discord')->getTypecast()[1] ?? null); + $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('discord')->getGenerated()); + + $this->assertTrue($fields->has('instagram')); + $this->assertTrue($fields->hasColumn('instagram')); + $this->assertSame('snowflake', $fields->get('instagram')->getType()); + $this->assertInstanceOf(SnowflakeFactory::class, $fields->get('instagram')->getTypecast()[0] ?? null); + $this->assertSame('createFromInteger', $fields->get('instagram')->getTypecast()[1] ?? null); + $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('instagram')->getGenerated()); + + $this->assertTrue($fields->has('mastodon')); + $this->assertTrue($fields->hasColumn('mastodon')); + $this->assertSame('snowflake', $fields->get('mastodon')->getType()); + $this->assertInstanceOf(SnowflakeFactory::class, $fields->get('mastodon')->getTypecast()[0] ?? null); + $this->assertSame('createFromInteger', $fields->get('mastodon')->getTypecast()[1] ?? null); + $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('mastodon')->getGenerated()); + + $this->assertTrue($fields->has('twitter')); + $this->assertTrue($fields->hasColumn('twitter')); + $this->assertSame('snowflake', $fields->get('twitter')->getType()); + $this->assertInstanceOf(SnowflakeFactory::class, $fields->get('twitter')->getTypecast()[0] ?? null); + $this->assertSame('createFromInteger', $fields->get('twitter')->getTypecast()[1] ?? null); + $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('twitter')->getGenerated()); + } + + /** + * @dataProvider readersDataProvider + */ + public function testAddNullableColumn(ReaderInterface $reader): void + { + $this->compileWithTokenizer($this->tokenizer, $reader); + + $fields = $this->registry->getEntity(NullableSnowflake::class)->getFields(); + + $this->assertTrue($fields->has('notDefinedSnowflake')); + $this->assertTrue($fields->hasColumn('not_defined_snowflake')); + $this->assertSame('snowflake', $fields->get('notDefinedSnowflake')->getType()); + $this->assertInstanceOf(SnowflakeFactory::class, $fields->get('notDefinedSnowflake')->getTypecast()[0] ?? null); + $this->assertSame('createFromInteger', $fields->get('notDefinedSnowflake')->getTypecast()[1] ?? null); + $this->assertTrue( + $this->registry + ->getTableSchema($this->registry->getEntity(NullableSnowflake::class)) + ->column('not_defined_snowflake') + ->isNullable(), + ); + $this->assertNull($fields->get('notDefinedSnowflake')->getGenerated()); + } + + #[\Override] + public function setUp(): void + { + parent::setUp(); + + $locator = new ClassLocator((new Finder())->files()->in([\dirname(__DIR__, 4) . '/Fixtures/Snowflake'])); + $reader = new AttributeReader(); + $this->tokenizer = new TokenizerEntityLocator($locator, $reader); + } +} diff --git a/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php index 8786353..1e29ed9 100644 --- a/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php @@ -92,6 +92,7 @@ public function withListeners(array|string $listeners): void ])); } + #[\Override] public function setUp(): void { parent::setUp(); diff --git a/tests/Identifier/Functional/Driver/Common/Ulid/UlidTest.php b/tests/Identifier/Functional/Driver/Common/Ulid/UlidTest.php index b35a08a..2388e84 100644 --- a/tests/Identifier/Functional/Driver/Common/Ulid/UlidTest.php +++ b/tests/Identifier/Functional/Driver/Common/Ulid/UlidTest.php @@ -106,6 +106,7 @@ public function testAddNullableColumn(ReaderInterface $reader): void $this->assertNull($fields->get('notDefinedUlid')->getGenerated()); } + #[\Override] public function setUp(): void { parent::setUp(); diff --git a/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php index f995299..0a5f8cb 100644 --- a/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php @@ -285,6 +285,7 @@ public function withListeners(array|string $listeners): void ])); } + #[\Override] public function setUp(): void { parent::setUp(); diff --git a/tests/Identifier/Functional/Driver/Common/Uuid/UuidTest.php b/tests/Identifier/Functional/Driver/Common/Uuid/UuidTest.php index b4bcf96..7bed336 100644 --- a/tests/Identifier/Functional/Driver/Common/Uuid/UuidTest.php +++ b/tests/Identifier/Functional/Driver/Common/Uuid/UuidTest.php @@ -112,6 +112,7 @@ public function testAddNullableColumn(ReaderInterface $reader): void $this->assertNull($fields->get('notDefinedUuid')->getGenerated()); } + #[\Override] public function setUp(): void { parent::setUp(); diff --git a/tests/Identifier/Functional/Driver/MySQL/Snowflake/ListenerTest.php b/tests/Identifier/Functional/Driver/MySQL/Snowflake/ListenerTest.php new file mode 100644 index 0000000..c215723 --- /dev/null +++ b/tests/Identifier/Functional/Driver/MySQL/Snowflake/ListenerTest.php @@ -0,0 +1,17 @@ + [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'workerId' => null, + 'processId' => null, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake'], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'workerId' => null, + 'processId' => null, + 'nullable' => true, + ], + ], + ], + ], + ['custom_snowflake', null, null, null, true], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'workerId' => 3, + 'processId' => 6, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake', null, 3, 6], + ]; + } + + /** + * @dataProvider schemaDataProvider + */ + public function testModifySchema(array $expected, array $args): void + { + $schema = []; + $snowflake = new SnowflakeDiscord(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + public function testModifySchemaWithDefaults(): void + { + Listener::setDefaults(1, 2); + + $args = ['snowflake', null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'workerId' => null, + 'processId' => null, + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new SnowflakeDiscord(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + #[\Override] + protected function setUp(): void + { + Listener::setDefaults(null, null); + + parent::setUp(); + } +} diff --git a/tests/Identifier/Unit/SnowflakeGenericTest.php b/tests/Identifier/Unit/SnowflakeGenericTest.php new file mode 100644 index 0000000..67b3402 --- /dev/null +++ b/tests/Identifier/Unit/SnowflakeGenericTest.php @@ -0,0 +1,113 @@ + [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'node' => null, + 'epochOffset' => null, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake'], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'node' => null, + 'epochOffset' => null, + 'nullable' => true, + ], + ], + ], + ], + ['custom_snowflake', null, null, null, true], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'node' => 3, + 'epochOffset' => 6, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake', null, 3, 6], + ]; + } + + /** + * @dataProvider schemaDataProvider + */ + public function testModifySchema(array $expected, array $args): void + { + $schema = []; + $snowflake = new SnowflakeGeneric(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + public function testModifySchemaWithDefaults(): void + { + Listener::setDefaults(1, 1738265600000); + + $args = ['snowflake', null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'node' => null, + 'epochOffset' => null, + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new SnowflakeGeneric(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + #[\Override] + protected function setUp(): void + { + Listener::setDefaults(null, null); + + parent::setUp(); + } +} diff --git a/tests/Identifier/Unit/SnowflakeInstagramTest.php b/tests/Identifier/Unit/SnowflakeInstagramTest.php new file mode 100644 index 0000000..fcfd2eb --- /dev/null +++ b/tests/Identifier/Unit/SnowflakeInstagramTest.php @@ -0,0 +1,109 @@ + [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'shardId' => null, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake'], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'shardId' => null, + 'nullable' => true, + ], + ], + ], + ], + ['custom_snowflake', null, null, true], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'shardId' => 3, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake', null, 3], + ]; + } + + /** + * @dataProvider schemaDataProvider + */ + public function testModifySchema(array $expected, array $args): void + { + $schema = []; + $snowflake = new SnowflakeInstagram(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + public function testModifySchemaWithDefaults(): void + { + Listener::setDefaults(1); + + $args = ['snowflake', null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'shardId' => null, + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new SnowflakeInstagram(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + #[\Override] + protected function setUp(): void + { + Listener::setDefaults(null); + + parent::setUp(); + } +} diff --git a/tests/Identifier/Unit/SnowflakeMastodonTest.php b/tests/Identifier/Unit/SnowflakeMastodonTest.php new file mode 100644 index 0000000..7920cc4 --- /dev/null +++ b/tests/Identifier/Unit/SnowflakeMastodonTest.php @@ -0,0 +1,109 @@ + [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'tableName' => null, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake'], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'tableName' => null, + 'nullable' => true, + ], + ], + ], + ], + ['custom_snowflake', null, null, true], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'tableName' => 'users', + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake', null, 'users'], + ]; + } + + /** + * @dataProvider schemaDataProvider + */ + public function testModifySchema(array $expected, array $args): void + { + $schema = []; + $snowflake = new SnowflakeMastodon(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + public function testModifySchemaWithDefaults(): void + { + Listener::setDefaults('users'); + + $args = ['snowflake', null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'tableName' => null, + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new SnowflakeMastodon(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + #[\Override] + protected function setUp(): void + { + Listener::setDefaults(null); + + parent::setUp(); + } +} diff --git a/tests/Identifier/Unit/SnowflakeTwitterTest.php b/tests/Identifier/Unit/SnowflakeTwitterTest.php new file mode 100644 index 0000000..5d31a18 --- /dev/null +++ b/tests/Identifier/Unit/SnowflakeTwitterTest.php @@ -0,0 +1,109 @@ + [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'machineId' => null, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake'], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'machineId' => null, + 'nullable' => true, + ], + ], + ], + ], + ['custom_snowflake', null, null, true], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'machineId' => 3, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake', null, 3], + ]; + } + + /** + * @dataProvider schemaDataProvider + */ + public function testModifySchema(array $expected, array $args): void + { + $schema = []; + $snowflake = new SnowflakeTwitter(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + public function testModifySchemaWithDefaults(): void + { + Listener::setDefaults(1); + + $args = ['snowflake', null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'machineId' => null, + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new SnowflakeTwitter(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + #[\Override] + protected function setUp(): void + { + Listener::setDefaults(null); + + parent::setUp(); + } +} diff --git a/tests/Identifier/Unit/UlidTest.php b/tests/Identifier/Unit/UlidTest.php index 6775d0c..7dbd6c3 100644 --- a/tests/Identifier/Unit/UlidTest.php +++ b/tests/Identifier/Unit/UlidTest.php @@ -6,7 +6,7 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\Ulid; -use Cycle\ORM\Entity\Behavior\Identifier\Listener\Ulid as UlidListener; +use Cycle\ORM\Entity\Behavior\Identifier\Listener\Ulid as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -18,7 +18,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => UlidListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'ulid', 'nullable' => false, @@ -32,7 +32,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => UlidListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_ulid', 'nullable' => false, @@ -46,7 +46,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => UlidListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_ulid', 'nullable' => true, diff --git a/tests/Identifier/Unit/Uuid1Test.php b/tests/Identifier/Unit/Uuid1Test.php index ed96b68..936f30f 100644 --- a/tests/Identifier/Unit/Uuid1Test.php +++ b/tests/Identifier/Unit/Uuid1Test.php @@ -107,4 +107,39 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Listener::setDefaults('foo', 1); + + $args = ['uuid', null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'uuid', + 'node' => null, + 'clockSeq' => null, + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new Uuid1(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + #[\Override] + protected function setUp(): void + { + Listener::setDefaults(null, null); + + parent::setUp(); + } } diff --git a/tests/Identifier/Unit/Uuid2Test.php b/tests/Identifier/Unit/Uuid2Test.php index 63fbeab..8fe19b6 100644 --- a/tests/Identifier/Unit/Uuid2Test.php +++ b/tests/Identifier/Unit/Uuid2Test.php @@ -136,4 +136,41 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Listener::setDefaults(DceDomain::Group, 2, 'foo', 3); + + $args = ['uuid', null, null, null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'uuid', + 'localDomain' => DceDomain::Person, + 'localIdentifier' => null, + 'node' => null, + 'clockSeq' => null, + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new Uuid2(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + #[\Override] + protected function setUp(): void + { + Listener::setDefaults(DceDomain::Person, null, null, null); + + parent::setUp(); + } } diff --git a/tests/Identifier/Unit/Uuid6Test.php b/tests/Identifier/Unit/Uuid6Test.php index 7c254a0..6c5a76b 100644 --- a/tests/Identifier/Unit/Uuid6Test.php +++ b/tests/Identifier/Unit/Uuid6Test.php @@ -107,4 +107,39 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Listener::setDefaults('foo', 1); + + $args = ['uuid', null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'uuid', + 'node' => null, + 'clockSeq' => null, + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new Uuid6(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + #[\Override] + protected function setUp(): void + { + Listener::setDefaults(null, null); + + parent::setUp(); + } }