From 5c5bf622fbf619d61b9d70d552ca99a21f4f2270 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Wed, 16 Jul 2025 16:50:04 +1000 Subject: [PATCH 01/20] Added support for Snowflake identifiers and all variations --- src/Listener/SnowflakeDiscord.php | 31 ++++++++++++ src/Listener/SnowflakeGeneric.php | 32 ++++++++++++ src/Listener/SnowflakeInstagram.php | 30 ++++++++++++ src/Listener/SnowflakeMastodon.php | 33 +++++++++++++ src/Listener/SnowflakeTwitter.php | 30 ++++++++++++ src/Snowflake.php | 62 ++++++++++++++++++++++++ src/SnowflakeDiscord.php | 73 ++++++++++++++++++++++++++++ src/SnowflakeGeneric.php | 75 +++++++++++++++++++++++++++++ src/SnowflakeInstagram.php | 69 ++++++++++++++++++++++++++ src/SnowflakeMastodon.php | 69 ++++++++++++++++++++++++++ src/SnowflakeTwitter.php | 69 ++++++++++++++++++++++++++ 11 files changed, 573 insertions(+) create mode 100644 src/Listener/SnowflakeDiscord.php create mode 100644 src/Listener/SnowflakeGeneric.php create mode 100644 src/Listener/SnowflakeInstagram.php create mode 100644 src/Listener/SnowflakeMastodon.php create mode 100644 src/Listener/SnowflakeTwitter.php create mode 100644 src/Snowflake.php create mode 100644 src/SnowflakeDiscord.php create mode 100644 src/SnowflakeGeneric.php create mode 100644 src/SnowflakeInstagram.php create mode 100644 src/SnowflakeMastodon.php create mode 100644 src/SnowflakeTwitter.php diff --git a/src/Listener/SnowflakeDiscord.php b/src/Listener/SnowflakeDiscord.php new file mode 100644 index 0000000..4bab364 --- /dev/null +++ b/src/Listener/SnowflakeDiscord.php @@ -0,0 +1,31 @@ +nullable || isset($event->state->getData()[$this->field])) { + return; + } + + $identifier = (new DiscordSnowflakeFactory($this->workerId, $this->processId))->create(); + + $event->state->register($this->field, $identifier); + } +} diff --git a/src/Listener/SnowflakeGeneric.php b/src/Listener/SnowflakeGeneric.php new file mode 100644 index 0000000..8654e87 --- /dev/null +++ b/src/Listener/SnowflakeGeneric.php @@ -0,0 +1,32 @@ +nullable || isset($event->state->getData()[$this->field])) { + return; + } + + $identifier = (new GenericSnowflakeFactory($this->node, $this->epochOffset))->create(); + + $event->state->register($this->field, $identifier); + } +} diff --git a/src/Listener/SnowflakeInstagram.php b/src/Listener/SnowflakeInstagram.php new file mode 100644 index 0000000..b89ecef --- /dev/null +++ b/src/Listener/SnowflakeInstagram.php @@ -0,0 +1,30 @@ +nullable || isset($event->state->getData()[$this->field])) { + return; + } + + $identifier = (new InstagramSnowflakeFactory($this->shardId))->create(); + + $event->state->register($this->field, $identifier); + } +} diff --git a/src/Listener/SnowflakeMastodon.php b/src/Listener/SnowflakeMastodon.php new file mode 100644 index 0000000..2ceaa9a --- /dev/null +++ b/src/Listener/SnowflakeMastodon.php @@ -0,0 +1,33 @@ +nullable || isset($event->state->getData()[$this->field])) { + return; + } + + $identifier = (new MastodonSnowflakeFactory($this->tableName))->create(); + + $event->state->register($this->field, $identifier); + } +} diff --git a/src/Listener/SnowflakeTwitter.php b/src/Listener/SnowflakeTwitter.php new file mode 100644 index 0000000..da83889 --- /dev/null +++ b/src/Listener/SnowflakeTwitter.php @@ -0,0 +1,30 @@ +nullable || isset($event->state->getData()[$this->field])) { + return; + } + + $identifier = (new TwitterSnowflakeFactory($this->machineId))->create(); + + $event->state->register($this->field, $identifier); + } +} diff --git a/src/Snowflake.php b/src/Snowflake.php new file mode 100644 index 0000000..edff9f5 --- /dev/null +++ b/src/Snowflake.php @@ -0,0 +1,62 @@ +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..89ae18e --- /dev/null +++ b/src/SnowflakeDiscord.php @@ -0,0 +1,73 @@ +field = $field; + $this->column = $column; + $this->nullable = $nullable; + } + + #[\Override] + protected function getListenerClass(): string + { + return Listener::class; + } + + #[ArrayShape([ + 'field' => 'string', + 'workerId' => 'int', + 'processId' => 'int', + '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..83fc323 --- /dev/null +++ b/src/SnowflakeGeneric.php @@ -0,0 +1,75 @@ +field = $field; + $this->column = $column; + $this->nullable = $nullable; + } + + #[\Override] + protected function getListenerClass(): string + { + return Listener::class; + } + + #[ArrayShape([ + 'field' => 'string', + 'node' => 'Epoch|int', + 'epochOffset' => 'int', + '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..e6039d7 --- /dev/null +++ b/src/SnowflakeInstagram.php @@ -0,0 +1,69 @@ +field = $field; + $this->column = $column; + $this->nullable = $nullable; + } + + #[\Override] + protected function getListenerClass(): string + { + return Listener::class; + } + + #[ArrayShape([ + 'field' => 'string', + 'shardId' => 'int', + '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..a2a5dba --- /dev/null +++ b/src/SnowflakeMastodon.php @@ -0,0 +1,69 @@ +field = $field; + $this->column = $column; + $this->nullable = $nullable; + } + + #[\Override] + protected function getListenerClass(): string + { + return Listener::class; + } + + #[ArrayShape([ + 'field' => 'string', + 'tableName' => '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..6e5b6fe --- /dev/null +++ b/src/SnowflakeTwitter.php @@ -0,0 +1,69 @@ +field = $field; + $this->column = $column; + $this->nullable = $nullable; + } + + #[\Override] + protected function getListenerClass(): string + { + return Listener::class; + } + + #[ArrayShape([ + 'field' => 'string', + 'machineId' => 'int', + '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); + } +} From 09c697e85bb4f0b289791eccde1628d8229bf95f Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Wed, 16 Jul 2025 16:51:10 +1000 Subject: [PATCH 02/20] Updated UUID and ULID keeping psalm satisfied --- src/Ulid.php | 3 ++- src/Uuid.php | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) 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( From ddb97c1d7b7bf9c8fc11d7c8e6023472bb2e0171 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Wed, 16 Jul 2025 16:52:24 +1000 Subject: [PATCH 03/20] Added tests for Snowflake identifiers as well as updated tests for UUID and ULID --- .../Fixtures/Combined/MultipleIdentifiers.php | 42 ++- .../Fixtures/Snowflake/MultipleSnowflake.php | 57 ++++ .../Fixtures/Snowflake/NullableSnowflake.php | 27 ++ tests/Identifier/Fixtures/Snowflake/Post.php | 24 ++ tests/Identifier/Fixtures/Snowflake/User.php | 25 ++ .../Identifier/Fixtures/Ulid/MultipleUlid.php | 22 +- .../Identifier/Fixtures/Ulid/NullableUlid.php | 14 +- tests/Identifier/Fixtures/Ulid/Post.php | 6 +- tests/Identifier/Fixtures/Ulid/User.php | 10 +- .../Identifier/Fixtures/Uuid/MultipleUuid.php | 19 +- .../Identifier/Fixtures/Uuid/NullableUuid.php | 10 +- tests/Identifier/Fixtures/Uuid/Post.php | 6 +- tests/Identifier/Fixtures/Uuid/User.php | 6 +- .../Driver/Common/Combined/CombinedTest.php | 1 + .../Driver/Common/Combined/ListenerTest.php | 35 ++- .../Driver/Common/Snowflake/ListenerTest.php | 288 ++++++++++++++++++ .../Driver/Common/Snowflake/SnowflakeTest.php | 139 +++++++++ .../Driver/Common/Ulid/ListenerTest.php | 1 + .../Driver/Common/Ulid/UlidTest.php | 1 + .../Driver/Common/Uuid/ListenerTest.php | 1 + .../Driver/Common/Uuid/UuidTest.php | 1 + .../Driver/MySQL/Snowflake/ListenerTest.php | 17 ++ .../Driver/MySQL/Snowflake/SnowflakeTest.php | 17 ++ .../Postgres/Snowflake/ListenerTest.php | 17 ++ .../Postgres/Snowflake/SnowflakeTest.php | 17 ++ .../SQLServer/Snowflake/ListenerTest.php | 17 ++ .../SQLServer/Snowflake/SnowflakeTest.php | 17 ++ .../Driver/SQLite/Snowflake/ListenerTest.php | 17 ++ .../Driver/SQLite/Snowflake/SnowflakeTest.php | 17 ++ .../Identifier/Unit/SnowflakeDiscordTest.php | 94 ++++++ .../Identifier/Unit/SnowflakeGenericTest.php | 94 ++++++ .../Unit/SnowflakeInstagramTest.php | 90 ++++++ .../Identifier/Unit/SnowflakeMastodonTest.php | 90 ++++++ .../Identifier/Unit/SnowflakeTwitterTest.php | 90 ++++++ 34 files changed, 1268 insertions(+), 61 deletions(-) create mode 100644 tests/Identifier/Fixtures/Snowflake/MultipleSnowflake.php create mode 100644 tests/Identifier/Fixtures/Snowflake/NullableSnowflake.php create mode 100644 tests/Identifier/Fixtures/Snowflake/Post.php create mode 100644 tests/Identifier/Fixtures/Snowflake/User.php create mode 100644 tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php create mode 100644 tests/Identifier/Functional/Driver/Common/Snowflake/SnowflakeTest.php create mode 100644 tests/Identifier/Functional/Driver/MySQL/Snowflake/ListenerTest.php create mode 100644 tests/Identifier/Functional/Driver/MySQL/Snowflake/SnowflakeTest.php create mode 100644 tests/Identifier/Functional/Driver/Postgres/Snowflake/ListenerTest.php create mode 100644 tests/Identifier/Functional/Driver/Postgres/Snowflake/SnowflakeTest.php create mode 100644 tests/Identifier/Functional/Driver/SQLServer/Snowflake/ListenerTest.php create mode 100644 tests/Identifier/Functional/Driver/SQLServer/Snowflake/SnowflakeTest.php create mode 100644 tests/Identifier/Functional/Driver/SQLite/Snowflake/ListenerTest.php create mode 100644 tests/Identifier/Functional/Driver/SQLite/Snowflake/SnowflakeTest.php create mode 100644 tests/Identifier/Unit/SnowflakeDiscordTest.php create mode 100644 tests/Identifier/Unit/SnowflakeGenericTest.php create mode 100644 tests/Identifier/Unit/SnowflakeInstagramTest.php create mode 100644 tests/Identifier/Unit/SnowflakeMastodonTest.php create mode 100644 tests/Identifier/Unit/SnowflakeTwitterTest.php 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 => SnowflakeDiscordListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'workerId' => 0, + 'processId' => 0, + 'nullable' => false, + ], + ], + ], + ], + [], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeDiscordListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'workerId' => 0, + 'processId' => 0, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake'], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeDiscordListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'workerId' => 0, + 'processId' => 0, + 'nullable' => true, + ], + ], + ], + ], + ['custom_snowflake', null, 0, 0, true], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeDiscordListener::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); + } +} diff --git a/tests/Identifier/Unit/SnowflakeGenericTest.php b/tests/Identifier/Unit/SnowflakeGenericTest.php new file mode 100644 index 0000000..c09c8a3 --- /dev/null +++ b/tests/Identifier/Unit/SnowflakeGenericTest.php @@ -0,0 +1,94 @@ + [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeGenericListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'node' => 0, + 'epochOffset' => 0, + 'nullable' => false, + ], + ], + ], + ], + [], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeGenericListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'node' => 0, + 'epochOffset' => 0, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake'], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeGenericListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'node' => 0, + 'epochOffset' => 0, + 'nullable' => true, + ], + ], + ], + ], + ['custom_snowflake', null, 0, 0, true], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeGenericListener::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); + } +} diff --git a/tests/Identifier/Unit/SnowflakeInstagramTest.php b/tests/Identifier/Unit/SnowflakeInstagramTest.php new file mode 100644 index 0000000..00a4442 --- /dev/null +++ b/tests/Identifier/Unit/SnowflakeInstagramTest.php @@ -0,0 +1,90 @@ + [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeInstagramListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'shardId' => 0, + 'nullable' => false, + ], + ], + ], + ], + [], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeInstagramListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'shardId' => 0, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake'], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeInstagramListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'shardId' => 0, + 'nullable' => true, + ], + ], + ], + ], + ['custom_snowflake', null, 0, true], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeInstagramListener::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); + } +} diff --git a/tests/Identifier/Unit/SnowflakeMastodonTest.php b/tests/Identifier/Unit/SnowflakeMastodonTest.php new file mode 100644 index 0000000..03ac06b --- /dev/null +++ b/tests/Identifier/Unit/SnowflakeMastodonTest.php @@ -0,0 +1,90 @@ + [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeMastodonListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'tableName' => null, + 'nullable' => false, + ], + ], + ], + ], + [], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeMastodonListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'tableName' => null, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake'], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeMastodonListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'tableName' => null, + 'nullable' => true, + ], + ], + ], + ], + ['custom_snowflake', null, null, true], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeMastodonListener::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); + } +} diff --git a/tests/Identifier/Unit/SnowflakeTwitterTest.php b/tests/Identifier/Unit/SnowflakeTwitterTest.php new file mode 100644 index 0000000..3fed01b --- /dev/null +++ b/tests/Identifier/Unit/SnowflakeTwitterTest.php @@ -0,0 +1,90 @@ + [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeTwitterListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'machineId' => 0, + 'nullable' => false, + ], + ], + ], + ], + [], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeTwitterListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'machineId' => 0, + 'nullable' => false, + ], + ], + ], + ], + ['custom_snowflake'], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeTwitterListener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'custom_snowflake', + 'machineId' => 0, + 'nullable' => true, + ], + ], + ], + ], + ['custom_snowflake', null, 0, true], + ]; + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => SnowflakeTwitterListener::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); + } +} From 5bc9eb0de71a94648f64f3497f4048e943196fe7 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Wed, 16 Jul 2025 16:52:34 +1000 Subject: [PATCH 04/20] Updated documentation --- README.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d6c358b..e49c945 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # 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 +19,90 @@ 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. -> **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. + +```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. + +```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. + +```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. + +```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 @@ -171,7 +252,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: From eca8ae46604c0ad8cbb41dadf38b9f1fa5249d0d Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Tue, 22 Jul 2025 16:28:40 +1000 Subject: [PATCH 05/20] Updated composer dependencies --- composer.lock | 43 ++++++++++++++++++++++--------------------- 1 file changed, 22 insertions(+), 21 deletions(-) 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", From 76ff3819f861a6789fee55b3410ec0f1675154dc Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 27 Jul 2025 22:23:42 +1000 Subject: [PATCH 06/20] Added classes for handling identifier default values --- src/Defaults/SnowflakeDiscord.php | 31 +++++++++++++ src/Defaults/SnowflakeGeneric.php | 33 ++++++++++++++ src/Defaults/SnowflakeInstagram.php | 20 +++++++++ src/Defaults/SnowflakeMastodon.php | 29 +++++++++++++ src/Defaults/SnowflakeTwitter.php | 20 +++++++++ src/Defaults/Uuid1.php | 43 ++++++++++++++++++ src/Defaults/Uuid2.php | 67 +++++++++++++++++++++++++++++ src/Defaults/Uuid6.php | 43 ++++++++++++++++++ 8 files changed, 286 insertions(+) create mode 100644 src/Defaults/SnowflakeDiscord.php create mode 100644 src/Defaults/SnowflakeGeneric.php create mode 100644 src/Defaults/SnowflakeInstagram.php create mode 100644 src/Defaults/SnowflakeMastodon.php create mode 100644 src/Defaults/SnowflakeTwitter.php create mode 100644 src/Defaults/Uuid1.php create mode 100644 src/Defaults/Uuid2.php create mode 100644 src/Defaults/Uuid6.php diff --git a/src/Defaults/SnowflakeDiscord.php b/src/Defaults/SnowflakeDiscord.php new file mode 100644 index 0000000..210f2c0 --- /dev/null +++ b/src/Defaults/SnowflakeDiscord.php @@ -0,0 +1,31 @@ +|non-empty-string|null $node + */ + private static Nic|int|string|null $node = null; + + private static ?int $clockSeq = null; + + /** + * @return Nic|int<0, 281474976710655>|non-empty-string|null + */ + public static function getNode(): Nic|int|string|null + { + return self::$node; + } + + /** + * @param Nic|int<0, 281474976710655>|non-empty-string|null $node + */ + public static function setNode(Nic|int|string|null $node): void + { + self::$node = $node; + } + + public static function getClockSeq(): ?int + { + return self::$clockSeq; + } + + public static function setClockSeq(?int $clockSeq): void + { + self::$clockSeq = $clockSeq; + } +} diff --git a/src/Defaults/Uuid2.php b/src/Defaults/Uuid2.php new file mode 100644 index 0000000..c3c601f --- /dev/null +++ b/src/Defaults/Uuid2.php @@ -0,0 +1,67 @@ +|non-empty-string|null $node + */ + private static Nic|int|string|null $node = null; + + private static ?int $clockSeq = null; + + public static function getLocalDomain(): DceDomain|int + { + return self::$localDomain; + } + + public static function setLocalDomain(DceDomain|int $localDomain): void + { + self::$localDomain = $localDomain; + } + + public static function getLocalIdentifier(): ?int + { + return self::$localIdentifier; + } + + public static function setLocalIdentifier(?int $localIdentifier): void + { + self::$localIdentifier = $localIdentifier; + } + + /** + * @return Nic|int<0, 281474976710655>|non-empty-string|null + */ + public static function getNode(): Nic|int|string|null + { + return self::$node; + } + + /** + * @param Nic|int<0, 281474976710655>|non-empty-string|null $node + */ + public static function setNode(Nic|int|string|null $node): void + { + self::$node = $node; + } + + public static function getClockSeq(): ?int + { + return self::$clockSeq; + } + + public static function setClockSeq(?int $clockSeq): void + { + self::$clockSeq = $clockSeq; + } +} diff --git a/src/Defaults/Uuid6.php b/src/Defaults/Uuid6.php new file mode 100644 index 0000000..ddbc89b --- /dev/null +++ b/src/Defaults/Uuid6.php @@ -0,0 +1,43 @@ +|non-empty-string|null $node + */ + private static Nic|int|string|null $node = null; + + private static ?int $clockSeq = null; + + /** + * @return Nic|int<0, 281474976710655>|non-empty-string|null + */ + public static function getNode(): Nic|int|string|null + { + return self::$node; + } + + /** + * @param Nic|int<0, 281474976710655>|non-empty-string|null $node + */ + public static function setNode(Nic|int|string|null $node): void + { + self::$node = $node; + } + + public static function getClockSeq(): ?int + { + return self::$clockSeq; + } + + public static function setClockSeq(?int $clockSeq): void + { + self::$clockSeq = $clockSeq; + } +} From 70758a10c61905a9ff73ace30ff15508fce02af0 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 27 Jul 2025 22:24:31 +1000 Subject: [PATCH 07/20] Integrated defaults into identifier classes --- src/SnowflakeDiscord.php | 14 ++++++++++---- src/SnowflakeGeneric.php | 14 ++++++++++---- src/SnowflakeInstagram.php | 8 ++++++-- src/SnowflakeMastodon.php | 9 ++++++++- src/SnowflakeTwitter.php | 8 ++++++-- src/Uuid1.php | 14 ++++++++++++-- src/Uuid2.php | 25 ++++++++++++++++++++----- src/Uuid6.php | 14 ++++++++++++-- 8 files changed, 84 insertions(+), 22 deletions(-) diff --git a/src/SnowflakeDiscord.php b/src/SnowflakeDiscord.php index 89ae18e..8627fec 100644 --- a/src/SnowflakeDiscord.php +++ b/src/SnowflakeDiscord.php @@ -5,6 +5,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeDiscord as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeDiscord as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use JetBrains\PhpStorm\ArrayShape; @@ -21,11 +22,14 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class SnowflakeDiscord extends BaseSnowflake { + private int $workerId; + private int $processId; + /** * @param non-empty-string $field Snowflake property name * @param string|null $column Snowflake column name - * @param int $workerId A worker identifier to use when creating Snowflakes - * @param int $processId A process identifier to use when creating Snowflakes + * @param int|null $workerId A worker identifier to use when creating Snowflakes + * @param int|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() @@ -33,13 +37,15 @@ final class SnowflakeDiscord extends BaseSnowflake public function __construct( string $field = 'snowflake', ?string $column = null, - private int $workerId = 0, - private int $processId = 0, + ?int $workerId = null, + ?int $processId = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; + $this->workerId = $workerId === null ? Defaults::getWorkerId() : $workerId; + $this->processId = $processId === null ? Defaults::getProcessId() : $processId; } #[\Override] diff --git a/src/SnowflakeGeneric.php b/src/SnowflakeGeneric.php index 83fc323..cb028db 100644 --- a/src/SnowflakeGeneric.php +++ b/src/SnowflakeGeneric.php @@ -5,6 +5,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeGeneric as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeGeneric as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use JetBrains\PhpStorm\ArrayShape; @@ -23,11 +24,14 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class SnowflakeGeneric extends BaseSnowflake { + private int $node; + private Epoch|int $epochOffset; + /** * @param non-empty-string $field Snowflake property name * @param string|null $column Snowflake column name - * @param int $node A node identifier to use when creating Snowflakes - * @param Epoch | int $epochOffset The offset from the Unix Epoch in milliseconds + * @param int|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() @@ -35,13 +39,15 @@ final class SnowflakeGeneric extends BaseSnowflake public function __construct( string $field = 'snowflake', ?string $column = null, - private int $node = 0, - private Epoch|int $epochOffset = 0, + ?int $node = null, + Epoch|int|null $epochOffset = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; + $this->node = $node === null ? Defaults::getNode() : $node; + $this->epochOffset = $epochOffset === null ? Defaults::getEpochOffset() : $epochOffset; } #[\Override] diff --git a/src/SnowflakeInstagram.php b/src/SnowflakeInstagram.php index e6039d7..06c355e 100644 --- a/src/SnowflakeInstagram.php +++ b/src/SnowflakeInstagram.php @@ -5,6 +5,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeInstagram as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeInstagram as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use JetBrains\PhpStorm\ArrayShape; @@ -21,10 +22,12 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class SnowflakeInstagram extends BaseSnowflake { + private int $shardId; + /** * @param non-empty-string $field Snowflake property name * @param string|null $column Snowflake column name - * @param int $shardId A shard identifier to use when creating Snowflakes + * @param int|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\DiscordSnowflakeFactory::create() @@ -32,12 +35,13 @@ final class SnowflakeInstagram extends BaseSnowflake public function __construct( string $field = 'snowflake', ?string $column = null, - private int $shardId = 0, + ?int $shardId = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; + $this->shardId = $shardId === null ? Defaults::getShardId() : $shardId; } #[\Override] diff --git a/src/SnowflakeMastodon.php b/src/SnowflakeMastodon.php index a2a5dba..d4d3e62 100644 --- a/src/SnowflakeMastodon.php +++ b/src/SnowflakeMastodon.php @@ -5,6 +5,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeMastodon as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeMastodon as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use JetBrains\PhpStorm\ArrayShape; @@ -21,6 +22,11 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class SnowflakeMastodon extends BaseSnowflake { + /** + * @var non-empty-string|null + */ + private ?string $tableName; + /** * @param non-empty-string $field Snowflake property name * @param string|null $column Snowflake column name @@ -32,12 +38,13 @@ final class SnowflakeMastodon extends BaseSnowflake public function __construct( string $field = 'snowflake', ?string $column = null, - private ?string $tableName = null, + ?string $tableName = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; + $this->tableName = $tableName === null ? Defaults::getTableName() : $tableName; } #[\Override] diff --git a/src/SnowflakeTwitter.php b/src/SnowflakeTwitter.php index 6e5b6fe..5a8b8cb 100644 --- a/src/SnowflakeTwitter.php +++ b/src/SnowflakeTwitter.php @@ -5,6 +5,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeTwitter as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeTwitter as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use JetBrains\PhpStorm\ArrayShape; @@ -21,10 +22,12 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class SnowflakeTwitter extends BaseSnowflake { + private int $machineId; + /** * @param non-empty-string $field Snowflake property name * @param string|null $column Snowflake column name - * @param int $machineId A machine identifier to use when creating Snowflakes + * @param int|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\DiscordSnowflakeFactory::create() @@ -32,12 +35,13 @@ final class SnowflakeTwitter extends BaseSnowflake public function __construct( string $field = 'snowflake', ?string $column = null, - private int $machineId = 0, + ?int $machineId = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; + $this->machineId = $machineId === null ? Defaults::getMachineId() : $machineId; } #[\Override] diff --git a/src/Uuid1.php b/src/Uuid1.php index efcb70e..2c93c78 100644 --- a/src/Uuid1.php +++ b/src/Uuid1.php @@ -4,6 +4,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid1 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid1 as Listener; use Cycle\ORM\Entity\Behavior\Identifier\Uuid as BaseUuid; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; @@ -21,6 +22,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 +44,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 === null ? Defaults::getNode() : $node; + $this->clockSeq = $clockSeq === null ? Defaults::getClockSeq() : $clockSeq; } #[\Override] diff --git a/src/Uuid2.php b/src/Uuid2.php index c3e1868..0a821f2 100644 --- a/src/Uuid2.php +++ b/src/Uuid2.php @@ -4,6 +4,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid2 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid2 as Listener; use Cycle\ORM\Entity\Behavior\Identifier\Uuid as BaseUuid; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; @@ -22,10 +23,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 +54,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 === null ? Defaults::getLocalDomain() : $localDomain; + $this->localIdentifier = $localIdentifier === null ? Defaults::getLocalIdentifier() : $localIdentifier; + $this->node = $node === null ? Defaults::getNode() : $node; + $this->clockSeq = $clockSeq === null ? Defaults::getClockSeq() : $clockSeq; } #[\Override] diff --git a/src/Uuid6.php b/src/Uuid6.php index 92d83c5..47e1fe9 100644 --- a/src/Uuid6.php +++ b/src/Uuid6.php @@ -4,6 +4,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid6 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid6 as Listener; use Cycle\ORM\Entity\Behavior\Identifier\Uuid as BaseUuid; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; @@ -21,6 +22,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 +44,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 === null ? Defaults::getNode() : $node; + $this->clockSeq = $clockSeq === null ? Defaults::getClockSeq() : $clockSeq; } #[\Override] From 289505f65d100b63a7de244b863da9d5812f6091 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 27 Jul 2025 22:24:59 +1000 Subject: [PATCH 08/20] Added tests for identifier defaults --- .../Identifier/Unit/SnowflakeDiscordTest.php | 50 +++++++++++++++++-- .../Identifier/Unit/SnowflakeGenericTest.php | 50 +++++++++++++++++-- .../Unit/SnowflakeInstagramTest.php | 46 +++++++++++++++-- .../Identifier/Unit/SnowflakeMastodonTest.php | 46 +++++++++++++++-- .../Identifier/Unit/SnowflakeTwitterTest.php | 46 +++++++++++++++-- tests/Identifier/Unit/UlidTest.php | 8 +-- tests/Identifier/Unit/Uuid1Test.php | 40 +++++++++++++++ tests/Identifier/Unit/Uuid2Test.php | 48 ++++++++++++++++++ tests/Identifier/Unit/Uuid6Test.php | 40 +++++++++++++++ 9 files changed, 345 insertions(+), 29 deletions(-) diff --git a/tests/Identifier/Unit/SnowflakeDiscordTest.php b/tests/Identifier/Unit/SnowflakeDiscordTest.php index 29d2531..292083d 100644 --- a/tests/Identifier/Unit/SnowflakeDiscordTest.php +++ b/tests/Identifier/Unit/SnowflakeDiscordTest.php @@ -6,7 +6,8 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeDiscord; -use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeDiscord as SnowflakeDiscordListener; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeDiscord as Defaults; +use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeDiscord as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -18,7 +19,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeDiscordListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'snowflake', 'workerId' => 0, @@ -34,7 +35,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeDiscordListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'workerId' => 0, @@ -50,7 +51,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeDiscordListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'workerId' => 0, @@ -66,7 +67,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeDiscordListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'workerId' => 3, @@ -91,4 +92,43 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Defaults::setWorkerId(1); + Defaults::setProcessId(2); + + $args = ['snowflake', null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'workerId' => Defaults::getWorkerId(), + 'processId' => Defaults::getProcessId(), + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new SnowflakeDiscord(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + $this->assertSame(1, Defaults::getWorkerId()); + $this->assertSame(2, Defaults::getProcessId()); + } + + #[\Override] + protected function setUp(): void + { + Defaults::setWorkerId(0); + Defaults::setProcessId(0); + + parent::setUp(); + } } diff --git a/tests/Identifier/Unit/SnowflakeGenericTest.php b/tests/Identifier/Unit/SnowflakeGenericTest.php index c09c8a3..fd2fbc1 100644 --- a/tests/Identifier/Unit/SnowflakeGenericTest.php +++ b/tests/Identifier/Unit/SnowflakeGenericTest.php @@ -6,7 +6,8 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeGeneric; -use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeGeneric as SnowflakeGenericListener; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeGeneric as Defaults; +use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeGeneric as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -18,7 +19,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeGenericListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'snowflake', 'node' => 0, @@ -34,7 +35,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeGenericListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'node' => 0, @@ -50,7 +51,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeGenericListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'node' => 0, @@ -66,7 +67,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeGenericListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'node' => 3, @@ -91,4 +92,43 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Defaults::setNode(1); + Defaults::setEpochOffset(1738265600000); + + $args = ['snowflake', null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'node' => Defaults::getNode(), + 'epochOffset' => Defaults::getEpochOffset(), + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new SnowflakeGeneric(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + $this->assertSame(1, Defaults::getNode()); + $this->assertSame(1738265600000, Defaults::getEpochOffset()); + } + + #[\Override] + protected function setUp(): void + { + Defaults::setNode(0); + Defaults::setEpochOffset(0); + + parent::setUp(); + } } diff --git a/tests/Identifier/Unit/SnowflakeInstagramTest.php b/tests/Identifier/Unit/SnowflakeInstagramTest.php index 00a4442..6ed9a03 100644 --- a/tests/Identifier/Unit/SnowflakeInstagramTest.php +++ b/tests/Identifier/Unit/SnowflakeInstagramTest.php @@ -6,7 +6,8 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeInstagram; -use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeInstagram as SnowflakeInstagramListener; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeInstagram as Defaults; +use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeInstagram as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -18,7 +19,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeInstagramListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'snowflake', 'shardId' => 0, @@ -33,7 +34,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeInstagramListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'shardId' => 0, @@ -48,7 +49,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeInstagramListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'shardId' => 0, @@ -63,7 +64,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeInstagramListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'shardId' => 3, @@ -87,4 +88,39 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Defaults::setShardId(1); + + $args = ['snowflake', null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'shardId' => Defaults::getShardId(), + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new SnowflakeInstagram(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + $this->assertSame(1, Defaults::getShardId()); + } + + #[\Override] + protected function setUp(): void + { + Defaults::setShardId(0); + + parent::setUp(); + } } diff --git a/tests/Identifier/Unit/SnowflakeMastodonTest.php b/tests/Identifier/Unit/SnowflakeMastodonTest.php index 03ac06b..c023fc5 100644 --- a/tests/Identifier/Unit/SnowflakeMastodonTest.php +++ b/tests/Identifier/Unit/SnowflakeMastodonTest.php @@ -6,7 +6,8 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeMastodon; -use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeMastodon as SnowflakeMastodonListener; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeMastodon as Defaults; +use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeMastodon as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -18,7 +19,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeMastodonListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'snowflake', 'tableName' => null, @@ -33,7 +34,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeMastodonListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'tableName' => null, @@ -48,7 +49,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeMastodonListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'tableName' => null, @@ -63,7 +64,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeMastodonListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'tableName' => 'users', @@ -87,4 +88,39 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Defaults::setTableName('users'); + + $args = ['snowflake', null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'tableName' => Defaults::getTableName(), + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new SnowflakeMastodon(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + $this->assertSame('users', Defaults::getTableName()); + } + + #[\Override] + protected function setUp(): void + { + Defaults::setTableName(null); + + parent::setUp(); + } } diff --git a/tests/Identifier/Unit/SnowflakeTwitterTest.php b/tests/Identifier/Unit/SnowflakeTwitterTest.php index 3fed01b..ce178d6 100644 --- a/tests/Identifier/Unit/SnowflakeTwitterTest.php +++ b/tests/Identifier/Unit/SnowflakeTwitterTest.php @@ -6,7 +6,8 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeTwitter; -use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeTwitter as SnowflakeTwitterListener; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeTwitter as Defaults; +use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeTwitter as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -18,7 +19,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeTwitterListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'snowflake', 'machineId' => 0, @@ -33,7 +34,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeTwitterListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'machineId' => 0, @@ -48,7 +49,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeTwitterListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'machineId' => 0, @@ -63,7 +64,7 @@ public static function schemaDataProvider(): \Traversable [ SchemaInterface::LISTENERS => [ [ - ListenerProvider::DEFINITION_CLASS => SnowflakeTwitterListener::class, + ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', 'machineId' => 3, @@ -87,4 +88,39 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Defaults::setMachineId(1); + + $args = ['snowflake', null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'snowflake', + 'machineId' => Defaults::getMachineId(), + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new SnowflakeTwitter(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + $this->assertSame(1, Defaults::getMachineId()); + } + + #[\Override] + protected function setUp(): void + { + Defaults::setMachineId(0); + + 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..92cd2b5 100644 --- a/tests/Identifier/Unit/Uuid1Test.php +++ b/tests/Identifier/Unit/Uuid1Test.php @@ -6,6 +6,7 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\Uuid1; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid1 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid1 as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -107,4 +108,43 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Defaults::setNode('foo'); + Defaults::setClockSeq(1); + + $args = ['uuid', null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'uuid', + 'node' => Defaults::getNode(), + 'clockSeq' => Defaults::getClockSeq(), + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new Uuid1(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + $this->assertSame('foo', Defaults::getNode()); + $this->assertSame(1, Defaults::getClockSeq()); + } + + #[\Override] + protected function setUp(): void + { + Defaults::setNode(null); + Defaults::setClockSeq(null); + + parent::setUp(); + } } diff --git a/tests/Identifier/Unit/Uuid2Test.php b/tests/Identifier/Unit/Uuid2Test.php index 63fbeab..a2ebd30 100644 --- a/tests/Identifier/Unit/Uuid2Test.php +++ b/tests/Identifier/Unit/Uuid2Test.php @@ -6,6 +6,7 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\Uuid2; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid2 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid2 as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -136,4 +137,51 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Defaults::setLocalDomain(DceDomain::Group); + Defaults::setLocalIdentifier(2); + Defaults::setNode('foo'); + Defaults::setClockSeq(3); + + $args = ['uuid', null, null, null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'uuid', + 'localDomain' => Defaults::getLocalDomain(), + 'localIdentifier' => Defaults::getLocalIdentifier(), + 'node' => Defaults::getNode(), + 'clockSeq' => Defaults::getClockSeq(), + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new Uuid2(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + $this->assertSame(DceDomain::Group, Defaults::getLocalDomain()); + $this->assertSame(2, Defaults::getLocalIdentifier()); + $this->assertSame('foo', Defaults::getNode()); + $this->assertSame(3, Defaults::getClockSeq()); + } + + #[\Override] + protected function setUp(): void + { + Defaults::setLocalDomain(0); + Defaults::setLocalIdentifier(null); + Defaults::setNode(null); + Defaults::setClockSeq(null); + + parent::setUp(); + } } diff --git a/tests/Identifier/Unit/Uuid6Test.php b/tests/Identifier/Unit/Uuid6Test.php index 7c254a0..a80b4fd 100644 --- a/tests/Identifier/Unit/Uuid6Test.php +++ b/tests/Identifier/Unit/Uuid6Test.php @@ -6,6 +6,7 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\Uuid6; +use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid6 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid6 as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -107,4 +108,43 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Defaults::setNode('foo'); + Defaults::setClockSeq(1); + + $args = ['uuid', null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'uuid', + 'node' => Defaults::getNode(), + 'clockSeq' => Defaults::getClockSeq(), + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $snowflake = new Uuid6(...$args); + $snowflake->modifySchema($schema); + + $this->assertSame($expected, $schema); + $this->assertSame('foo', Defaults::getNode()); + $this->assertSame(1, Defaults::getClockSeq()); + } + + #[\Override] + protected function setUp(): void + { + Defaults::setNode(null); + Defaults::setClockSeq(null); + + parent::setUp(); + } } From f9b8cc49d31a346ae976047145bdc8730ca24661 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 27 Jul 2025 22:35:43 +1000 Subject: [PATCH 09/20] Updated documentation --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e49c945..873ea83 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ composer require cycle/entity-behavior-identifier ## Snowflake Examples -**Generic:** A flexible Snowflake format that can use a node identifier and any epoch offset, suitable for various applications requiring unique identifiers. +**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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeGeneric`. ```php use Cycle\Annotated\Annotation\Column; @@ -36,7 +36,7 @@ class User } ``` -**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. +**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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeDiscord`. ```php use Cycle\Annotated\Annotation\Column; @@ -53,7 +53,7 @@ class User } ``` -**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. +**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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeInstagram`. ```php use Cycle\Annotated\Annotation\Column; @@ -70,7 +70,7 @@ class User } ``` -**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. +**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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeMastodon`. ```php use Cycle\Annotated\Annotation\Column; @@ -87,7 +87,7 @@ class User } ``` -**Twitter:** Snowflake identifier for Twitter (X), beginning from `2010-11-04`. Can incorporate a machine ID to generate distinct Snowflakes. +**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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeTwitter`. ```php use Cycle\Annotated\Annotation\Column; @@ -125,7 +125,7 @@ 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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid1`. ```php use Cycle\Annotated\Annotation\Column; @@ -142,7 +142,7 @@ 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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid2`. ```php use Cycle\Annotated\Annotation\Column; @@ -218,7 +218,7 @@ 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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid6`. ```php use Cycle\Annotated\Annotation\Column; From df00756e4feefde9afb155afe2779bbe865e0e23 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 27 Jul 2025 22:44:12 +1000 Subject: [PATCH 10/20] Updated documentation --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 873ea83..c306922 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) [![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) From 759cd178a26d33c262ef81888886325d027daace Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 10 Aug 2025 12:14:35 +0400 Subject: [PATCH 11/20] Add base Snowflake listener --- src/Listener/Snowflake.php | 32 ++++++++++++++++++++++++++++++++ src/Snowflake.php | 3 +++ 2 files changed, 35 insertions(+) create mode 100644 src/Listener/Snowflake.php 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/Snowflake.php b/src/Snowflake.php index edff9f5..5797d51 100644 --- a/src/Snowflake.php +++ b/src/Snowflake.php @@ -12,7 +12,10 @@ abstract class Snowflake extends BaseModifier { + /** @var non-empty-string|null */ protected ?string $column = null; + + /** @var non-empty-string */ protected string $field; protected bool $nullable = false; From ed571a20442526f4109e91e6b25f6daa8074dcaf Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 10 Aug 2025 12:50:47 +0400 Subject: [PATCH 12/20] Refactor SnowflakeDiscord --- README.md | 3 +- src/Defaults/SnowflakeDiscord.php | 31 -------- src/Listener/SnowflakeDiscord.php | 75 +++++++++++++++---- src/SnowflakeDiscord.php | 33 ++++---- .../Identifier/Unit/SnowflakeDiscordTest.php | 39 +++------- 5 files changed, 85 insertions(+), 96 deletions(-) delete mode 100644 src/Defaults/SnowflakeDiscord.php diff --git a/README.md b/README.md index c306922..9038364 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,8 @@ class User } ``` -**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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeDiscord`. +### 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; diff --git a/src/Defaults/SnowflakeDiscord.php b/src/Defaults/SnowflakeDiscord.php deleted file mode 100644 index 210f2c0..0000000 --- a/src/Defaults/SnowflakeDiscord.php +++ /dev/null @@ -1,31 +0,0 @@ - */ + private static int $workerId = 0; + + /** @var null|int<0, 281474976710655> */ + private static ?int $processId = 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( - private string $field = 'snowflake', - private int $workerId = 0, - private int $processId = 0, - private bool $nullable = false, - ) {} - - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void + string $field, + bool $nullable = false, + ?int $workerId = null, + ?int $processId = null, + ) { + $workerId ??= self::$workerId; + $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 ($this->nullable || isset($event->state->getData()[$this->field])) { - return; + 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.'); } - $identifier = (new DiscordSnowflakeFactory($this->workerId, $this->processId))->create(); + self::$workerId = (int) $workerId; + self::$processId = $processId; + } - $event->state->register($this->field, $identifier); + #[\Override] + protected function createValue(): DiscordSnowflake + { + return $this->factory->create(); + } + + /** + * Get the current process ID. + * + * @return int<0, 281474976710655> + */ + private function getProcessId(): int + { + return self::$processId ??= \getmypid(); } } diff --git a/src/SnowflakeDiscord.php b/src/SnowflakeDiscord.php index 8627fec..bba5f6e 100644 --- a/src/SnowflakeDiscord.php +++ b/src/SnowflakeDiscord.php @@ -5,10 +5,8 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeDiscord as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeDiscord as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; -use JetBrains\PhpStorm\ArrayShape; use Ramsey\Identifier\Snowflake\DiscordSnowflakeFactory; use Ramsey\Identifier\SnowflakeFactory; @@ -22,30 +20,25 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class SnowflakeDiscord extends BaseSnowflake { - private int $workerId; - private int $processId; - /** * @param non-empty-string $field Snowflake property name - * @param string|null $column Snowflake column name - * @param int|null $workerId A worker identifier to use when creating Snowflakes - * @param int|null $processId A process identifier to use when creating Snowflakes + * @param non-empty-string|null $column Snowflake column name + * @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 * @param bool $nullable Indicates whether to generate a new Snowflake or not * * @see \Ramsey\Identifier\Snowflake\DiscordSnowflakeFactory::create() */ public function __construct( - string $field = 'snowflake', + string $field, ?string $column = null, - ?int $workerId = null, - ?int $processId = null, + private readonly ?int $workerId = null, + private readonly ?int $processId = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; - $this->workerId = $workerId === null ? Defaults::getWorkerId() : $workerId; - $this->processId = $processId === null ? Defaults::getProcessId() : $processId; } #[\Override] @@ -54,12 +47,14 @@ protected function getListenerClass(): string return Listener::class; } - #[ArrayShape([ - 'field' => 'string', - 'workerId' => 'int', - 'processId' => 'int', - 'nullable' => 'bool', - ])] + /** + * @return array{ + * field: non-empty-string, + * workerId: null|int<0, 281474976710655>, + * processId: null|int<0, 281474976710655>, + * nullable: bool + * } + */ #[\Override] protected function getListenerArgs(): array { diff --git a/tests/Identifier/Unit/SnowflakeDiscordTest.php b/tests/Identifier/Unit/SnowflakeDiscordTest.php index 292083d..8fd1c74 100644 --- a/tests/Identifier/Unit/SnowflakeDiscordTest.php +++ b/tests/Identifier/Unit/SnowflakeDiscordTest.php @@ -6,7 +6,6 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeDiscord; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeDiscord as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeDiscord as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -15,22 +14,6 @@ final class SnowflakeDiscordTest extends TestCase { public static function schemaDataProvider(): \Traversable { - yield [ - [ - SchemaInterface::LISTENERS => [ - [ - ListenerProvider::DEFINITION_CLASS => Listener::class, - ListenerProvider::DEFINITION_ARGS => [ - 'field' => 'snowflake', - 'workerId' => 0, - 'processId' => 0, - 'nullable' => false, - ], - ], - ], - ], - [], - ]; yield [ [ SchemaInterface::LISTENERS => [ @@ -38,8 +21,8 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', - 'workerId' => 0, - 'processId' => 0, + 'workerId' => null, + 'processId' => null, 'nullable' => false, ], ], @@ -54,14 +37,14 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', - 'workerId' => 0, - 'processId' => 0, + 'workerId' => null, + 'processId' => null, 'nullable' => true, ], ], ], ], - ['custom_snowflake', null, 0, 0, true], + ['custom_snowflake', null, null, null, true], ]; yield [ [ @@ -95,8 +78,7 @@ public function testModifySchema(array $expected, array $args): void public function testModifySchemaWithDefaults(): void { - Defaults::setWorkerId(1); - Defaults::setProcessId(2); + Listener::setDefaults(1, 2); $args = ['snowflake', null, null, null, false]; @@ -106,8 +88,8 @@ public function testModifySchemaWithDefaults(): void ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'snowflake', - 'workerId' => Defaults::getWorkerId(), - 'processId' => Defaults::getProcessId(), + 'workerId' => null, + 'processId' => null, 'nullable' => false, ], ], @@ -119,15 +101,12 @@ public function testModifySchemaWithDefaults(): void $snowflake->modifySchema($schema); $this->assertSame($expected, $schema); - $this->assertSame(1, Defaults::getWorkerId()); - $this->assertSame(2, Defaults::getProcessId()); } #[\Override] protected function setUp(): void { - Defaults::setWorkerId(0); - Defaults::setProcessId(0); + Listener::setDefaults(null, null); parent::setUp(); } From 5130bbb5c53c01393d287a321e7f47546e34d0a8 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 10 Aug 2025 13:09:58 +0400 Subject: [PATCH 13/20] Refactor SnowflakeInstagram --- README.md | 3 +- src/Defaults/SnowflakeInstagram.php | 20 -------- src/Listener/SnowflakeInstagram.php | 51 ++++++++++++++----- src/Snowflake.php | 1 + src/SnowflakeInstagram.php | 27 +++++----- .../Unit/SnowflakeInstagramTest.php | 29 +++-------- 6 files changed, 59 insertions(+), 72 deletions(-) delete mode 100644 src/Defaults/SnowflakeInstagram.php diff --git a/README.md b/README.md index 9038364..14a7cfc 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,8 @@ class User } ``` -**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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeInstagram`. +### 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; diff --git a/src/Defaults/SnowflakeInstagram.php b/src/Defaults/SnowflakeInstagram.php deleted file mode 100644 index f32a474..0000000 --- a/src/Defaults/SnowflakeInstagram.php +++ /dev/null @@ -1,20 +0,0 @@ - */ + 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( - private string $field = 'snowflake', - private int $shardId = 0, - private bool $nullable = false, - ) {} + string $field, + bool $nullable = false, + ?int $shardId = null, + ) { + $shardId ??= self::$shardId; + $this->factory = new InstagramSnowflakeFactory($shardId); + parent::__construct($field, $nullable); + } - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void + /** + * 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 ($this->nullable || isset($event->state->getData()[$this->field])) { - return; + if ($shardId !== null && ($shardId < 0 || $shardId > 1023)) { + throw new \InvalidArgumentException('Shard ID must be between 0 and 1023.'); } - $identifier = (new InstagramSnowflakeFactory($this->shardId))->create(); + self::$shardId = (int) $shardId; + } - $event->state->register($this->field, $identifier); + #[\Override] + protected function createValue(): InstagramSnowflake + { + return $this->factory->create(); } } diff --git a/src/Snowflake.php b/src/Snowflake.php index 5797d51..a360c7c 100644 --- a/src/Snowflake.php +++ b/src/Snowflake.php @@ -17,6 +17,7 @@ abstract class Snowflake extends BaseModifier /** @var non-empty-string */ protected string $field; + protected bool $nullable = false; #[\Override] diff --git a/src/SnowflakeInstagram.php b/src/SnowflakeInstagram.php index 06c355e..cc08026 100644 --- a/src/SnowflakeInstagram.php +++ b/src/SnowflakeInstagram.php @@ -5,10 +5,8 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeInstagram as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeInstagram as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; -use JetBrains\PhpStorm\ArrayShape; use Ramsey\Identifier\Snowflake\InstagramSnowflakeFactory; use Ramsey\Identifier\SnowflakeFactory; @@ -22,26 +20,23 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class SnowflakeInstagram extends BaseSnowflake { - private int $shardId; - /** * @param non-empty-string $field Snowflake property name - * @param string|null $column Snowflake column name - * @param int|null $shardId A shard identifier to use when creating Snowflakes + * @param non-empty-string|null $column Snowflake column name + * @param int<0, 1023>|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\DiscordSnowflakeFactory::create() + * @see \Ramsey\Identifier\Snowflake\InstagramSnowflakeFactory::create() */ public function __construct( - string $field = 'snowflake', + string $field, ?string $column = null, - ?int $shardId = null, + private readonly ?int $shardId = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; - $this->shardId = $shardId === null ? Defaults::getShardId() : $shardId; } #[\Override] @@ -50,11 +45,13 @@ protected function getListenerClass(): string return Listener::class; } - #[ArrayShape([ - 'field' => 'string', - 'shardId' => 'int', - 'nullable' => 'bool', - ])] + /** + * @return array{ + * field: non-empty-string, + * shardId: null|int<0, 1023>, + * nullable: bool + * } + */ #[\Override] protected function getListenerArgs(): array { diff --git a/tests/Identifier/Unit/SnowflakeInstagramTest.php b/tests/Identifier/Unit/SnowflakeInstagramTest.php index 6ed9a03..fcfd2eb 100644 --- a/tests/Identifier/Unit/SnowflakeInstagramTest.php +++ b/tests/Identifier/Unit/SnowflakeInstagramTest.php @@ -6,7 +6,6 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeInstagram; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeInstagram as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeInstagram as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -15,21 +14,6 @@ final class SnowflakeInstagramTest extends TestCase { public static function schemaDataProvider(): \Traversable { - yield [ - [ - SchemaInterface::LISTENERS => [ - [ - ListenerProvider::DEFINITION_CLASS => Listener::class, - ListenerProvider::DEFINITION_ARGS => [ - 'field' => 'snowflake', - 'shardId' => 0, - 'nullable' => false, - ], - ], - ], - ], - [], - ]; yield [ [ SchemaInterface::LISTENERS => [ @@ -37,7 +21,7 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', - 'shardId' => 0, + 'shardId' => null, 'nullable' => false, ], ], @@ -52,13 +36,13 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', - 'shardId' => 0, + 'shardId' => null, 'nullable' => true, ], ], ], ], - ['custom_snowflake', null, 0, true], + ['custom_snowflake', null, null, true], ]; yield [ [ @@ -91,7 +75,7 @@ public function testModifySchema(array $expected, array $args): void public function testModifySchemaWithDefaults(): void { - Defaults::setShardId(1); + Listener::setDefaults(1); $args = ['snowflake', null, null, false]; @@ -101,7 +85,7 @@ public function testModifySchemaWithDefaults(): void ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'snowflake', - 'shardId' => Defaults::getShardId(), + 'shardId' => null, 'nullable' => false, ], ], @@ -113,13 +97,12 @@ public function testModifySchemaWithDefaults(): void $snowflake->modifySchema($schema); $this->assertSame($expected, $schema); - $this->assertSame(1, Defaults::getShardId()); } #[\Override] protected function setUp(): void { - Defaults::setShardId(0); + Listener::setDefaults(null); parent::setUp(); } From bf24d19ed11ab1d1a7aac16bf33ddc0504040d5b Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 10 Aug 2025 13:19:15 +0400 Subject: [PATCH 14/20] Refactor other Snowflakes --- README.md | 9 ++- src/Defaults/SnowflakeGeneric.php | 33 ---------- src/Defaults/SnowflakeMastodon.php | 29 --------- src/Defaults/SnowflakeTwitter.php | 20 ------ src/Listener/SnowflakeGeneric.php | 64 ++++++++++++++----- src/Listener/SnowflakeMastodon.php | 50 ++++++++++----- src/Listener/SnowflakeTwitter.php | 51 +++++++++++---- src/SnowflakeGeneric.php | 31 ++++----- src/SnowflakeMastodon.php | 28 ++++---- src/SnowflakeTwitter.php | 27 ++++---- .../Identifier/Unit/SnowflakeGenericTest.php | 39 +++-------- .../Identifier/Unit/SnowflakeMastodonTest.php | 23 +------ .../Identifier/Unit/SnowflakeTwitterTest.php | 29 ++------- 13 files changed, 181 insertions(+), 252 deletions(-) delete mode 100644 src/Defaults/SnowflakeGeneric.php delete mode 100644 src/Defaults/SnowflakeMastodon.php delete mode 100644 src/Defaults/SnowflakeTwitter.php diff --git a/README.md b/README.md index 14a7cfc..a1a465f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ composer require cycle/entity-behavior-identifier ## Snowflake Examples -**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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeGeneric`. +### 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. ```php use Cycle\Annotated\Annotation\Column; @@ -71,7 +72,8 @@ class User } ``` -**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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeMastodon`. +### 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; @@ -88,7 +90,8 @@ class User } ``` -**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 class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeTwitter`. +### 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; diff --git a/src/Defaults/SnowflakeGeneric.php b/src/Defaults/SnowflakeGeneric.php deleted file mode 100644 index 68397d2..0000000 --- a/src/Defaults/SnowflakeGeneric.php +++ /dev/null @@ -1,33 +0,0 @@ - */ + private static int $node = 0; + + /** @var Epoch|int */ + 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( - private string $field = 'snowflake', - private int $node = 0, - private Epoch|int $epochOffset = 0, - private bool $nullable = false, - ) {} - - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void + 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 ($this->nullable || isset($event->state->getData()[$this->field])) { - return; + if ($node !== null && ($node < 0 || $node > 1023)) { + throw new \InvalidArgumentException('Node ID must be between 0 and 1023.'); } - $identifier = (new GenericSnowflakeFactory($this->node, $this->epochOffset))->create(); + self::$node = (int) $node; + if ($epochOffset !== null) { + self::$epochOffset = $epochOffset; + } + } - $event->state->register($this->field, $identifier); + #[\Override] + protected function createValue(): Snowflake + { + return $this->factory->create(); } } diff --git a/src/Listener/SnowflakeMastodon.php b/src/Listener/SnowflakeMastodon.php index 2ceaa9a..235c39d 100644 --- a/src/Listener/SnowflakeMastodon.php +++ b/src/Listener/SnowflakeMastodon.php @@ -4,30 +4,48 @@ 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\Snowflake\MastodonSnowflake; use Ramsey\Identifier\Snowflake\MastodonSnowflakeFactory; -final class SnowflakeMastodon +/** + * Generates Mastodon Snowflake identifiers for entities. + * You can set default table name using the {@see setDefaults()} method. + */ +final class SnowflakeMastodon extends Snowflake { + /** @var non-empty-string|null */ + private static ?string $tableName = null; + + private MastodonSnowflakeFactory $factory; + /** - * @param non-empty-string|null $tableName + * @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 non-empty-string|null $tableName Database table name ensuring different tables derive separate sequence bases */ public function __construct( - private string $field = 'snowflake', - private ?string $tableName = null, - private bool $nullable = false, - ) {} + string $field, + bool $nullable = false, + ?string $tableName = null, + ) { + $tableName ??= self::$tableName; + $this->factory = new MastodonSnowflakeFactory($tableName); + parent::__construct($field, $nullable); + } - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void + /** + * 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 { - if ($this->nullable || isset($event->state->getData()[$this->field])) { - return; - } - - $identifier = (new MastodonSnowflakeFactory($this->tableName))->create(); + self::$tableName = $tableName; + } - $event->state->register($this->field, $identifier); + #[\Override] + protected function createValue(): MastodonSnowflake + { + return $this->factory->create(); } } diff --git a/src/Listener/SnowflakeTwitter.php b/src/Listener/SnowflakeTwitter.php index da83889..ce6a942 100644 --- a/src/Listener/SnowflakeTwitter.php +++ b/src/Listener/SnowflakeTwitter.php @@ -4,27 +4,52 @@ 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\Snowflake\TwitterSnowflake; use Ramsey\Identifier\Snowflake\TwitterSnowflakeFactory; -final class SnowflakeTwitter +/** + * Generates Twitter Snowflake identifiers for entities. + * You can set default machine ID using the {@see setDefaults()} method. + */ +final class SnowflakeTwitter extends Snowflake { + /** @var int<0, 1023> */ + 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( - private string $field = 'snowflake', - private int $machineId = 0, - private bool $nullable = false, - ) {} + string $field, + bool $nullable = false, + ?int $machineId = null, + ) { + $machineId ??= self::$machineId; + $this->factory = new TwitterSnowflakeFactory($machineId); + parent::__construct($field, $nullable); + } - #[Listen(OnCreate::class)] - public function __invoke(OnCreate $event): void + /** + * 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 ($this->nullable || isset($event->state->getData()[$this->field])) { - return; + if ($machineId !== null && ($machineId < 0 || $machineId > 1023)) { + throw new \InvalidArgumentException('Machine ID must be between 0 and 1023.'); } - $identifier = (new TwitterSnowflakeFactory($this->machineId))->create(); + self::$machineId = (int) $machineId; + } - $event->state->register($this->field, $identifier); + #[\Override] + protected function createValue(): TwitterSnowflake + { + return $this->factory->create(); } } diff --git a/src/SnowflakeGeneric.php b/src/SnowflakeGeneric.php index cb028db..eca47d7 100644 --- a/src/SnowflakeGeneric.php +++ b/src/SnowflakeGeneric.php @@ -5,10 +5,8 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeGeneric as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeGeneric as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; -use JetBrains\PhpStorm\ArrayShape; use Ramsey\Identifier\Snowflake\Epoch; use Ramsey\Identifier\Snowflake\GenericSnowflakeFactory; use Ramsey\Identifier\SnowflakeFactory; @@ -24,30 +22,25 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class SnowflakeGeneric extends BaseSnowflake { - private int $node; - private Epoch|int $epochOffset; - /** * @param non-empty-string $field Snowflake property name - * @param string|null $column Snowflake column name - * @param int|null $node A node identifier to use when creating Snowflakes + * @param non-empty-string|null $column Snowflake column name + * @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 * @param bool $nullable Indicates whether to generate a new Snowflake or not * * @see \Ramsey\Identifier\Snowflake\GenericSnowflakeFactory::create() */ public function __construct( - string $field = 'snowflake', + string $field, ?string $column = null, - ?int $node = null, - Epoch|int|null $epochOffset = 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; - $this->node = $node === null ? Defaults::getNode() : $node; - $this->epochOffset = $epochOffset === null ? Defaults::getEpochOffset() : $epochOffset; } #[\Override] @@ -56,12 +49,14 @@ protected function getListenerClass(): string return Listener::class; } - #[ArrayShape([ - 'field' => 'string', - 'node' => 'Epoch|int', - 'epochOffset' => 'int', - 'nullable' => 'bool', - ])] + /** + * @return array{ + * field: non-empty-string, + * node: null|int<0, 1023>, + * epochOffset: Epoch|int|null, + * nullable: bool + * } + */ #[\Override] protected function getListenerArgs(): array { diff --git a/src/SnowflakeMastodon.php b/src/SnowflakeMastodon.php index d4d3e62..6abf0c8 100644 --- a/src/SnowflakeMastodon.php +++ b/src/SnowflakeMastodon.php @@ -5,10 +5,8 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeMastodon as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeMastodon as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; -use JetBrains\PhpStorm\ArrayShape; use Ramsey\Identifier\Snowflake\MastodonSnowflakeFactory; use Ramsey\Identifier\SnowflakeFactory; @@ -22,29 +20,23 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class SnowflakeMastodon extends BaseSnowflake { - /** - * @var non-empty-string|null - */ - private ?string $tableName; - /** * @param non-empty-string $field Snowflake property name - * @param string|null $column Snowflake column name + * @param non-empty-string|null $column Snowflake column name * @param non-empty-string|null $tableName Database table name ensuring different tables derive separate sequence bases * @param bool $nullable Indicates whether to generate a new Snowflake or not * - * @see \Ramsey\Identifier\Snowflake\GenericSnowflakeFactory::create() + * @see \Ramsey\Identifier\Snowflake\MastodonSnowflakeFactory::create() */ public function __construct( - string $field = 'snowflake', + string $field, ?string $column = null, - ?string $tableName = null, + private readonly ?string $tableName = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; - $this->tableName = $tableName === null ? Defaults::getTableName() : $tableName; } #[\Override] @@ -53,11 +45,13 @@ protected function getListenerClass(): string return Listener::class; } - #[ArrayShape([ - 'field' => 'string', - 'tableName' => 'string|null', - 'nullable' => 'bool', - ])] + /** + * @return array{ + * field: non-empty-string, + * tableName: non-empty-string|null, + * nullable: bool + * } + */ #[\Override] protected function getListenerArgs(): array { diff --git a/src/SnowflakeTwitter.php b/src/SnowflakeTwitter.php index 5a8b8cb..f079464 100644 --- a/src/SnowflakeTwitter.php +++ b/src/SnowflakeTwitter.php @@ -5,10 +5,8 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeTwitter as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeTwitter as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; -use JetBrains\PhpStorm\ArrayShape; use Ramsey\Identifier\Snowflake\TwitterSnowflakeFactory; use Ramsey\Identifier\SnowflakeFactory; @@ -22,26 +20,23 @@ #[\Attribute(\Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE), NamedArgumentConstructor] final class SnowflakeTwitter extends BaseSnowflake { - private int $machineId; - /** * @param non-empty-string $field Snowflake property name - * @param string|null $column Snowflake column name - * @param int|null $machineId A machine identifier to use when creating Snowflakes + * @param non-empty-string|null $column Snowflake column name + * @param int<0, 1023>|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\DiscordSnowflakeFactory::create() + * @see \Ramsey\Identifier\Snowflake\TwitterSnowflakeFactory::create() */ public function __construct( - string $field = 'snowflake', + string $field, ?string $column = null, - ?int $machineId = null, + private readonly ?int $machineId = null, bool $nullable = false, ) { $this->field = $field; $this->column = $column; $this->nullable = $nullable; - $this->machineId = $machineId === null ? Defaults::getMachineId() : $machineId; } #[\Override] @@ -50,11 +45,13 @@ protected function getListenerClass(): string return Listener::class; } - #[ArrayShape([ - 'field' => 'string', - 'machineId' => 'int', - 'nullable' => 'bool', - ])] + /** + * @return array{ + * field: non-empty-string, + * machineId: null|int<0, 1023>, + * nullable: bool + * } + */ #[\Override] protected function getListenerArgs(): array { diff --git a/tests/Identifier/Unit/SnowflakeGenericTest.php b/tests/Identifier/Unit/SnowflakeGenericTest.php index fd2fbc1..67b3402 100644 --- a/tests/Identifier/Unit/SnowflakeGenericTest.php +++ b/tests/Identifier/Unit/SnowflakeGenericTest.php @@ -6,7 +6,6 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeGeneric; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeGeneric as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeGeneric as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -15,22 +14,6 @@ final class SnowflakeGenericTest extends TestCase { public static function schemaDataProvider(): \Traversable { - yield [ - [ - SchemaInterface::LISTENERS => [ - [ - ListenerProvider::DEFINITION_CLASS => Listener::class, - ListenerProvider::DEFINITION_ARGS => [ - 'field' => 'snowflake', - 'node' => 0, - 'epochOffset' => 0, - 'nullable' => false, - ], - ], - ], - ], - [], - ]; yield [ [ SchemaInterface::LISTENERS => [ @@ -38,8 +21,8 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', - 'node' => 0, - 'epochOffset' => 0, + 'node' => null, + 'epochOffset' => null, 'nullable' => false, ], ], @@ -54,14 +37,14 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', - 'node' => 0, - 'epochOffset' => 0, + 'node' => null, + 'epochOffset' => null, 'nullable' => true, ], ], ], ], - ['custom_snowflake', null, 0, 0, true], + ['custom_snowflake', null, null, null, true], ]; yield [ [ @@ -95,8 +78,7 @@ public function testModifySchema(array $expected, array $args): void public function testModifySchemaWithDefaults(): void { - Defaults::setNode(1); - Defaults::setEpochOffset(1738265600000); + Listener::setDefaults(1, 1738265600000); $args = ['snowflake', null, null, null, false]; @@ -106,8 +88,8 @@ public function testModifySchemaWithDefaults(): void ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'snowflake', - 'node' => Defaults::getNode(), - 'epochOffset' => Defaults::getEpochOffset(), + 'node' => null, + 'epochOffset' => null, 'nullable' => false, ], ], @@ -119,15 +101,12 @@ public function testModifySchemaWithDefaults(): void $snowflake->modifySchema($schema); $this->assertSame($expected, $schema); - $this->assertSame(1, Defaults::getNode()); - $this->assertSame(1738265600000, Defaults::getEpochOffset()); } #[\Override] protected function setUp(): void { - Defaults::setNode(0); - Defaults::setEpochOffset(0); + Listener::setDefaults(null, null); parent::setUp(); } diff --git a/tests/Identifier/Unit/SnowflakeMastodonTest.php b/tests/Identifier/Unit/SnowflakeMastodonTest.php index c023fc5..7920cc4 100644 --- a/tests/Identifier/Unit/SnowflakeMastodonTest.php +++ b/tests/Identifier/Unit/SnowflakeMastodonTest.php @@ -6,7 +6,6 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeMastodon; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeMastodon as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeMastodon as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -15,21 +14,6 @@ final class SnowflakeMastodonTest extends TestCase { public static function schemaDataProvider(): \Traversable { - yield [ - [ - SchemaInterface::LISTENERS => [ - [ - ListenerProvider::DEFINITION_CLASS => Listener::class, - ListenerProvider::DEFINITION_ARGS => [ - 'field' => 'snowflake', - 'tableName' => null, - 'nullable' => false, - ], - ], - ], - ], - [], - ]; yield [ [ SchemaInterface::LISTENERS => [ @@ -91,7 +75,7 @@ public function testModifySchema(array $expected, array $args): void public function testModifySchemaWithDefaults(): void { - Defaults::setTableName('users'); + Listener::setDefaults('users'); $args = ['snowflake', null, null, false]; @@ -101,7 +85,7 @@ public function testModifySchemaWithDefaults(): void ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'snowflake', - 'tableName' => Defaults::getTableName(), + 'tableName' => null, 'nullable' => false, ], ], @@ -113,13 +97,12 @@ public function testModifySchemaWithDefaults(): void $snowflake->modifySchema($schema); $this->assertSame($expected, $schema); - $this->assertSame('users', Defaults::getTableName()); } #[\Override] protected function setUp(): void { - Defaults::setTableName(null); + Listener::setDefaults(null); parent::setUp(); } diff --git a/tests/Identifier/Unit/SnowflakeTwitterTest.php b/tests/Identifier/Unit/SnowflakeTwitterTest.php index ce178d6..5d31a18 100644 --- a/tests/Identifier/Unit/SnowflakeTwitterTest.php +++ b/tests/Identifier/Unit/SnowflakeTwitterTest.php @@ -6,7 +6,6 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeTwitter; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\SnowflakeTwitter as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeTwitter as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -15,21 +14,6 @@ final class SnowflakeTwitterTest extends TestCase { public static function schemaDataProvider(): \Traversable { - yield [ - [ - SchemaInterface::LISTENERS => [ - [ - ListenerProvider::DEFINITION_CLASS => Listener::class, - ListenerProvider::DEFINITION_ARGS => [ - 'field' => 'snowflake', - 'machineId' => 0, - 'nullable' => false, - ], - ], - ], - ], - [], - ]; yield [ [ SchemaInterface::LISTENERS => [ @@ -37,7 +21,7 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', - 'machineId' => 0, + 'machineId' => null, 'nullable' => false, ], ], @@ -52,13 +36,13 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_snowflake', - 'machineId' => 0, + 'machineId' => null, 'nullable' => true, ], ], ], ], - ['custom_snowflake', null, 0, true], + ['custom_snowflake', null, null, true], ]; yield [ [ @@ -91,7 +75,7 @@ public function testModifySchema(array $expected, array $args): void public function testModifySchemaWithDefaults(): void { - Defaults::setMachineId(1); + Listener::setDefaults(1); $args = ['snowflake', null, null, false]; @@ -101,7 +85,7 @@ public function testModifySchemaWithDefaults(): void ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'snowflake', - 'machineId' => Defaults::getMachineId(), + 'machineId' => null, 'nullable' => false, ], ], @@ -113,13 +97,12 @@ public function testModifySchemaWithDefaults(): void $snowflake->modifySchema($schema); $this->assertSame($expected, $schema); - $this->assertSame(1, Defaults::getMachineId()); } #[\Override] protected function setUp(): void { - Defaults::setMachineId(0); + Listener::setDefaults(null); parent::setUp(); } From e51f8319bafca1ee4f5a449f2661c3ee5f9a4e2b Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Sun, 10 Aug 2025 20:13:01 +0400 Subject: [PATCH 15/20] Refactor UUID listeners --- README.md | 9 ++- src/Defaults/Uuid1.php | 43 ------------- src/Defaults/Uuid2.php | 67 -------------------- src/Defaults/Uuid6.php | 43 ------------- src/Listener/BaseUuid.php | 32 ++++++++++ src/Listener/SnowflakeDiscord.php | 12 ++-- src/Listener/SnowflakeGeneric.php | 1 - src/Listener/Uuid1.php | 63 ++++++++++++------- src/Listener/Uuid2.php | 94 ++++++++++++++++++++--------- src/Listener/Uuid3.php | 46 +++++++------- src/Listener/Uuid4.php | 36 ++++++----- src/Listener/Uuid5.php | 46 +++++++------- src/Listener/Uuid6.php | 66 ++++++++++++-------- src/Listener/Uuid7.php | 36 ++++++----- src/Uuid1.php | 5 +- src/Uuid2.php | 9 ++- src/Uuid6.php | 5 +- tests/Identifier/Unit/Uuid1Test.php | 13 ++-- tests/Identifier/Unit/Uuid2Test.php | 23 ++----- tests/Identifier/Unit/Uuid6Test.php | 13 ++-- 20 files changed, 304 insertions(+), 358 deletions(-) delete mode 100644 src/Defaults/Uuid1.php delete mode 100644 src/Defaults/Uuid2.php delete mode 100644 src/Defaults/Uuid6.php create mode 100644 src/Listener/BaseUuid.php diff --git a/README.md b/README.md index a1a465f..176d275 100644 --- a/README.md +++ b/README.md @@ -129,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. Default values for `node` and `clockSeq` can be defined globally via class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid1`. +### 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; @@ -146,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. Default values for `localDomain`, `localIdentifier`, `node` and `clockSeq` can be defined globally via class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid2`. +### 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; @@ -222,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). Default values for `node` and `clockSeq` can be defined globally via class `Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid6`. +### 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; diff --git a/src/Defaults/Uuid1.php b/src/Defaults/Uuid1.php deleted file mode 100644 index 23d719d..0000000 --- a/src/Defaults/Uuid1.php +++ /dev/null @@ -1,43 +0,0 @@ -|non-empty-string|null $node - */ - private static Nic|int|string|null $node = null; - - private static ?int $clockSeq = null; - - /** - * @return Nic|int<0, 281474976710655>|non-empty-string|null - */ - public static function getNode(): Nic|int|string|null - { - return self::$node; - } - - /** - * @param Nic|int<0, 281474976710655>|non-empty-string|null $node - */ - public static function setNode(Nic|int|string|null $node): void - { - self::$node = $node; - } - - public static function getClockSeq(): ?int - { - return self::$clockSeq; - } - - public static function setClockSeq(?int $clockSeq): void - { - self::$clockSeq = $clockSeq; - } -} diff --git a/src/Defaults/Uuid2.php b/src/Defaults/Uuid2.php deleted file mode 100644 index c3c601f..0000000 --- a/src/Defaults/Uuid2.php +++ /dev/null @@ -1,67 +0,0 @@ -|non-empty-string|null $node - */ - private static Nic|int|string|null $node = null; - - private static ?int $clockSeq = null; - - public static function getLocalDomain(): DceDomain|int - { - return self::$localDomain; - } - - public static function setLocalDomain(DceDomain|int $localDomain): void - { - self::$localDomain = $localDomain; - } - - public static function getLocalIdentifier(): ?int - { - return self::$localIdentifier; - } - - public static function setLocalIdentifier(?int $localIdentifier): void - { - self::$localIdentifier = $localIdentifier; - } - - /** - * @return Nic|int<0, 281474976710655>|non-empty-string|null - */ - public static function getNode(): Nic|int|string|null - { - return self::$node; - } - - /** - * @param Nic|int<0, 281474976710655>|non-empty-string|null $node - */ - public static function setNode(Nic|int|string|null $node): void - { - self::$node = $node; - } - - public static function getClockSeq(): ?int - { - return self::$clockSeq; - } - - public static function setClockSeq(?int $clockSeq): void - { - self::$clockSeq = $clockSeq; - } -} diff --git a/src/Defaults/Uuid6.php b/src/Defaults/Uuid6.php deleted file mode 100644 index ddbc89b..0000000 --- a/src/Defaults/Uuid6.php +++ /dev/null @@ -1,43 +0,0 @@ -|non-empty-string|null $node - */ - private static Nic|int|string|null $node = null; - - private static ?int $clockSeq = null; - - /** - * @return Nic|int<0, 281474976710655>|non-empty-string|null - */ - public static function getNode(): Nic|int|string|null - { - return self::$node; - } - - /** - * @param Nic|int<0, 281474976710655>|non-empty-string|null $node - */ - public static function setNode(Nic|int|string|null $node): void - { - self::$node = $node; - } - - public static function getClockSeq(): ?int - { - return self::$clockSeq; - } - - public static function setClockSeq(?int $clockSeq): void - { - self::$clockSeq = $clockSeq; - } -} 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/SnowflakeDiscord.php b/src/Listener/SnowflakeDiscord.php index cbee2d1..510447f 100644 --- a/src/Listener/SnowflakeDiscord.php +++ b/src/Listener/SnowflakeDiscord.php @@ -14,10 +14,10 @@ final class SnowflakeDiscord extends Snowflake { /** @var int<0, 281474976710655> */ - private static int $workerId = 0; + private static int $defaultWorkerId = 0; /** @var null|int<0, 281474976710655> */ - private static ?int $processId = null; + private static ?int $defaultProcessId = null; private DiscordSnowflakeFactory $factory; @@ -33,7 +33,7 @@ public function __construct( ?int $workerId = null, ?int $processId = null, ) { - $workerId ??= self::$workerId; + $workerId ??= self::$defaultWorkerId; $processId ??= $this->getProcessId(); $this->factory = new DiscordSnowflakeFactory($workerId, $processId); parent::__construct($field, $nullable); @@ -54,8 +54,8 @@ public static function setDefaults(?int $workerId, ?int $processId): void throw new \InvalidArgumentException('Process ID must be between 0 and 281474976710655.'); } - self::$workerId = (int) $workerId; - self::$processId = $processId; + self::$defaultWorkerId = (int) $workerId; + self::$defaultProcessId = $processId; } #[\Override] @@ -71,6 +71,6 @@ protected function createValue(): DiscordSnowflake */ private function getProcessId(): int { - return self::$processId ??= \getmypid(); + return self::$defaultProcessId ??= \getmypid(); } } diff --git a/src/Listener/SnowflakeGeneric.php b/src/Listener/SnowflakeGeneric.php index 3e035ab..4110282 100644 --- a/src/Listener/SnowflakeGeneric.php +++ b/src/Listener/SnowflakeGeneric.php @@ -17,7 +17,6 @@ final class SnowflakeGeneric extends \Cycle\ORM\Entity\Behavior\Identifier\Liste /** @var int<0, 1023> */ private static int $node = 0; - /** @var Epoch|int */ private static Epoch|int $epochOffset = 0; private GenericSnowflakeFactory $factory; 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..b6a100e 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; - } - - $this->localDomain = \is_int($this->localDomain) ? DceDomain::from($this->localDomain) : $this->localDomain; + 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); + } - $identifier = (new UuidFactory())->v2( - $this->localDomain, - $this->localIdentifier, - $this->node, - $this->clockSeq, - ); + /** + * 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; + } - $event->state->register($this->field, $identifier); + #[\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; + + $localDomain = \is_int($localDomain) ? DceDomain::from($localDomain) : $localDomain; + + 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/Uuid1.php b/src/Uuid1.php index 2c93c78..0afae6a 100644 --- a/src/Uuid1.php +++ b/src/Uuid1.php @@ -4,7 +4,6 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid1 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid1 as Listener; use Cycle\ORM\Entity\Behavior\Identifier\Uuid as BaseUuid; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; @@ -51,8 +50,8 @@ public function __construct( $this->field = $field; $this->column = $column; $this->nullable = $nullable; - $this->node = $node === null ? Defaults::getNode() : $node; - $this->clockSeq = $clockSeq === null ? Defaults::getClockSeq() : $clockSeq; + $this->node = $node; + $this->clockSeq = $clockSeq; } #[\Override] diff --git a/src/Uuid2.php b/src/Uuid2.php index 0a821f2..424be17 100644 --- a/src/Uuid2.php +++ b/src/Uuid2.php @@ -4,7 +4,6 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid2 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid2 as Listener; use Cycle\ORM\Entity\Behavior\Identifier\Uuid as BaseUuid; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; @@ -63,10 +62,10 @@ public function __construct( $this->field = $field; $this->column = $column; $this->nullable = $nullable; - $this->localDomain = $localDomain === null ? Defaults::getLocalDomain() : $localDomain; - $this->localIdentifier = $localIdentifier === null ? Defaults::getLocalIdentifier() : $localIdentifier; - $this->node = $node === null ? Defaults::getNode() : $node; - $this->clockSeq = $clockSeq === null ? Defaults::getClockSeq() : $clockSeq; + $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 47e1fe9..ce71544 100644 --- a/src/Uuid6.php +++ b/src/Uuid6.php @@ -4,7 +4,6 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid6 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid6 as Listener; use Cycle\ORM\Entity\Behavior\Identifier\Uuid as BaseUuid; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; @@ -51,8 +50,8 @@ public function __construct( $this->field = $field; $this->column = $column; $this->nullable = $nullable; - $this->node = $node === null ? Defaults::getNode() : $node; - $this->clockSeq = $clockSeq === null ? Defaults::getClockSeq() : $clockSeq; + $this->node = $node; + $this->clockSeq = $clockSeq; } #[\Override] diff --git a/tests/Identifier/Unit/Uuid1Test.php b/tests/Identifier/Unit/Uuid1Test.php index 92cd2b5..936f30f 100644 --- a/tests/Identifier/Unit/Uuid1Test.php +++ b/tests/Identifier/Unit/Uuid1Test.php @@ -6,7 +6,6 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\Uuid1; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid1 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid1 as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -111,8 +110,7 @@ public function testModifySchema(array $expected, array $args): void public function testModifySchemaWithDefaults(): void { - Defaults::setNode('foo'); - Defaults::setClockSeq(1); + Listener::setDefaults('foo', 1); $args = ['uuid', null, null, null, false]; @@ -122,8 +120,8 @@ public function testModifySchemaWithDefaults(): void ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'uuid', - 'node' => Defaults::getNode(), - 'clockSeq' => Defaults::getClockSeq(), + 'node' => null, + 'clockSeq' => null, 'nullable' => false, ], ], @@ -135,15 +133,12 @@ public function testModifySchemaWithDefaults(): void $snowflake->modifySchema($schema); $this->assertSame($expected, $schema); - $this->assertSame('foo', Defaults::getNode()); - $this->assertSame(1, Defaults::getClockSeq()); } #[\Override] protected function setUp(): void { - Defaults::setNode(null); - Defaults::setClockSeq(null); + Listener::setDefaults(null, null); parent::setUp(); } diff --git a/tests/Identifier/Unit/Uuid2Test.php b/tests/Identifier/Unit/Uuid2Test.php index a2ebd30..8fe19b6 100644 --- a/tests/Identifier/Unit/Uuid2Test.php +++ b/tests/Identifier/Unit/Uuid2Test.php @@ -6,7 +6,6 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\Uuid2; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid2 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid2 as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -140,10 +139,7 @@ public function testModifySchema(array $expected, array $args): void public function testModifySchemaWithDefaults(): void { - Defaults::setLocalDomain(DceDomain::Group); - Defaults::setLocalIdentifier(2); - Defaults::setNode('foo'); - Defaults::setClockSeq(3); + Listener::setDefaults(DceDomain::Group, 2, 'foo', 3); $args = ['uuid', null, null, null, null, null, false]; @@ -153,10 +149,10 @@ public function testModifySchemaWithDefaults(): void ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'uuid', - 'localDomain' => Defaults::getLocalDomain(), - 'localIdentifier' => Defaults::getLocalIdentifier(), - 'node' => Defaults::getNode(), - 'clockSeq' => Defaults::getClockSeq(), + 'localDomain' => DceDomain::Person, + 'localIdentifier' => null, + 'node' => null, + 'clockSeq' => null, 'nullable' => false, ], ], @@ -168,19 +164,12 @@ public function testModifySchemaWithDefaults(): void $snowflake->modifySchema($schema); $this->assertSame($expected, $schema); - $this->assertSame(DceDomain::Group, Defaults::getLocalDomain()); - $this->assertSame(2, Defaults::getLocalIdentifier()); - $this->assertSame('foo', Defaults::getNode()); - $this->assertSame(3, Defaults::getClockSeq()); } #[\Override] protected function setUp(): void { - Defaults::setLocalDomain(0); - Defaults::setLocalIdentifier(null); - Defaults::setNode(null); - Defaults::setClockSeq(null); + Listener::setDefaults(DceDomain::Person, null, null, null); parent::setUp(); } diff --git a/tests/Identifier/Unit/Uuid6Test.php b/tests/Identifier/Unit/Uuid6Test.php index a80b4fd..6c5a76b 100644 --- a/tests/Identifier/Unit/Uuid6Test.php +++ b/tests/Identifier/Unit/Uuid6Test.php @@ -6,7 +6,6 @@ use Cycle\ORM\Entity\Behavior\Dispatcher\ListenerProvider; use Cycle\ORM\Entity\Behavior\Identifier\Uuid6; -use Cycle\ORM\Entity\Behavior\Identifier\Defaults\Uuid6 as Defaults; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid6 as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; @@ -111,8 +110,7 @@ public function testModifySchema(array $expected, array $args): void public function testModifySchemaWithDefaults(): void { - Defaults::setNode('foo'); - Defaults::setClockSeq(1); + Listener::setDefaults('foo', 1); $args = ['uuid', null, null, null, false]; @@ -122,8 +120,8 @@ public function testModifySchemaWithDefaults(): void ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'uuid', - 'node' => Defaults::getNode(), - 'clockSeq' => Defaults::getClockSeq(), + 'node' => null, + 'clockSeq' => null, 'nullable' => false, ], ], @@ -135,15 +133,12 @@ public function testModifySchemaWithDefaults(): void $snowflake->modifySchema($schema); $this->assertSame($expected, $schema); - $this->assertSame('foo', Defaults::getNode()); - $this->assertSame(1, Defaults::getClockSeq()); } #[\Override] protected function setUp(): void { - Defaults::setNode(null); - Defaults::setClockSeq(null); + Listener::setDefaults(null, null); parent::setUp(); } From 61ec5ef8b1fdeaffa6d1f8631a68463344339cfd Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 10 Aug 2025 16:13:48 +0000 Subject: [PATCH 16/20] style(php-cs-fixer): fix coding standards --- src/Listener/SnowflakeGeneric.php | 1 - src/Listener/Uuid2.php | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Listener/SnowflakeGeneric.php b/src/Listener/SnowflakeGeneric.php index 4110282..c20343b 100644 --- a/src/Listener/SnowflakeGeneric.php +++ b/src/Listener/SnowflakeGeneric.php @@ -18,7 +18,6 @@ final class SnowflakeGeneric extends \Cycle\ORM\Entity\Behavior\Identifier\Liste private static int $node = 0; private static Epoch|int $epochOffset = 0; - private GenericSnowflakeFactory $factory; /** diff --git a/src/Listener/Uuid2.php b/src/Listener/Uuid2.php index b6a100e..353c56b 100644 --- a/src/Listener/Uuid2.php +++ b/src/Listener/Uuid2.php @@ -73,9 +73,9 @@ protected function createValue(): \Ramsey\Identifier\Uuid $localIdentifier = $this->localIdentifier ?? self::$defaultLocalIdentifier; $node = $this->node ?? self::$defaultNode; $clockSeq = $this->clockSeq ?? self::$defaultClockSeq; - + $localDomain = \is_int($localDomain) ? DceDomain::from($localDomain) : $localDomain; - + return $this->factory->create($localDomain, $localIdentifier, $node, $clockSeq); } } From fb6df5028a07ff6e9ef5ea5c14709927a25947d2 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Wed, 20 Aug 2025 03:40:55 +1000 Subject: [PATCH 17/20] Provides fixes after merged changes broke a lot of things --- src/Listener/SnowflakeDiscord.php | 4 +- src/Listener/Uuid3.php | 10 ++--- src/Listener/Uuid5.php | 10 ++--- src/Snowflake.php | 22 +++++++---- src/SnowflakeDiscord.php | 23 +++++++++-- src/SnowflakeGeneric.php | 23 +++++++++-- src/SnowflakeInstagram.php | 21 ++++++++-- src/SnowflakeMastodon.php | 21 ++++++++-- src/SnowflakeTwitter.php | 21 ++++++++-- .../Driver/Common/Snowflake/ListenerTest.php | 12 +++++- .../Driver/Common/Snowflake/SnowflakeTest.php | 17 -------- .../Driver/Common/Uuid/ListenerTest.php | 39 ++++++++++++++++--- 12 files changed, 159 insertions(+), 64 deletions(-) diff --git a/src/Listener/SnowflakeDiscord.php b/src/Listener/SnowflakeDiscord.php index 510447f..86744d7 100644 --- a/src/Listener/SnowflakeDiscord.php +++ b/src/Listener/SnowflakeDiscord.php @@ -66,11 +66,9 @@ protected function createValue(): DiscordSnowflake /** * Get the current process ID. - * - * @return int<0, 281474976710655> */ private function getProcessId(): int { - return self::$defaultProcessId ??= \getmypid(); + return self::$defaultProcessId === null ? \intval(\getmypid()) : self::$defaultProcessId; } } diff --git a/src/Listener/Uuid3.php b/src/Listener/Uuid3.php index 7adb4e1..40d830b 100644 --- a/src/Listener/Uuid3.php +++ b/src/Listener/Uuid3.php @@ -7,18 +7,18 @@ use Cycle\ORM\Entity\Behavior\Identifier\Listener\BaseUuid as Base; use Ramsey\Identifier\Uuid; use Ramsey\Identifier\Uuid\NamespaceId; -use Ramsey\Identifier\Uuid\UuidV3Factory; +use Ramsey\Identifier\Uuid\UuidFactory; /** * Generates UUIDv3 (name-based with MD5 hashing) identifiers for entities. */ final class Uuid3 extends Base { - private UuidV3Factory $factory; + private UuidFactory $factory; /** * @param non-empty-string $field The name of the field to store the UUID - * @param NamespaceId|BaseUuid|string $namespace The namespace UUID + * @param NamespaceId|Uuid|string $namespace The namespace UUID * @param string $name The name to hash * @param bool $nullable Indicates whether the UUID can be null */ @@ -28,13 +28,13 @@ public function __construct( private readonly string $name, bool $nullable = false, ) { - $this->factory = new UuidV3Factory(); + $this->factory = new UuidFactory(); parent::__construct($field, $nullable); } #[\Override] protected function createValue(): \Ramsey\Identifier\Uuid { - return $this->factory->create($this->namespace, $this->name); + return $this->factory->v3($this->namespace, $this->name); } } diff --git a/src/Listener/Uuid5.php b/src/Listener/Uuid5.php index e14a6dd..7156f28 100644 --- a/src/Listener/Uuid5.php +++ b/src/Listener/Uuid5.php @@ -7,18 +7,18 @@ use Cycle\ORM\Entity\Behavior\Identifier\Listener\BaseUuid as Base; use Ramsey\Identifier\Uuid; use Ramsey\Identifier\Uuid\NamespaceId; -use Ramsey\Identifier\Uuid\UuidV5Factory; +use Ramsey\Identifier\Uuid\UuidFactory; /** * Generates UUIDv5 (name-based with SHA-1 hashing) identifiers for entities. */ final class Uuid5 extends Base { - private UuidV5Factory $factory; + private UuidFactory $factory; /** * @param non-empty-string $field The name of the field to store the UUID - * @param NamespaceId|BaseUuid|string $namespace The namespace UUID + * @param NamespaceId|Uuid|string $namespace The namespace UUID * @param string $name The name to hash * @param bool $nullable Indicates whether the UUID can be null */ @@ -28,13 +28,13 @@ public function __construct( private readonly string $name, bool $nullable = false, ) { - $this->factory = new UuidV5Factory(); + $this->factory = new UuidFactory(); parent::__construct($field, $nullable); } #[\Override] protected function createValue(): \Ramsey\Identifier\Uuid { - return $this->factory->create($this->namespace, $this->name); + return $this->factory->v5($this->namespace, $this->name); } } diff --git a/src/Snowflake.php b/src/Snowflake.php index a360c7c..d01237e 100644 --- a/src/Snowflake.php +++ b/src/Snowflake.php @@ -4,11 +4,11 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; +use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Schema\BaseModifier; use Cycle\ORM\Entity\Behavior\Schema\RegistryModifier; use Cycle\ORM\Schema\GeneratedField; use Cycle\Schema\Registry; -use Ramsey\Identifier\SnowflakeFactory; abstract class Snowflake extends BaseModifier { @@ -24,6 +24,7 @@ abstract class Snowflake extends BaseModifier public function compute(Registry $registry): void { $modifier = new RegistryModifier($registry, $this->role); + /** @var non-empty-string column */ $this->column = $modifier->findColumnName($this->field, $this->column); if (\is_string($this->column) && $this->column !== '') { $modifier->addSnowflakeColumn( @@ -32,11 +33,9 @@ public function compute(Registry $registry): void $this->nullable ? null : GeneratedField::BEFORE_INSERT, )->nullable($this->nullable); - $factory = $this->snowflakeFactory(); - $modifier->setTypecast( $registry->getEntity($this->role)->getFields()->get($this->field), - [$factory, 'createFromInteger'], + [static::class, 'fromInteger', $this->getTypecastArgs()], ); } } @@ -54,13 +53,20 @@ public function render(Registry $registry): void $this->nullable ? null : GeneratedField::BEFORE_INSERT, )->nullable($this->nullable); - $factory = $this->snowflakeFactory(); - $modifier->setTypecast( $registry->getEntity($this->role)->getFields()->get($this->field), - [$factory, 'createFromInteger'], + [static::class, 'fromInteger', $this->getTypecastArgs()], ); } - abstract protected function snowflakeFactory(): SnowflakeFactory; + /** + * @param int<0, max>|numeric-string $identifier + */ + abstract public static function fromInteger( + int|string $identifier, + DatabaseInterface $database, + array $arguments, + ): \Ramsey\Identifier\Snowflake; + + abstract protected function getTypecastArgs(): array; } diff --git a/src/SnowflakeDiscord.php b/src/SnowflakeDiscord.php index bba5f6e..6e7280f 100644 --- a/src/SnowflakeDiscord.php +++ b/src/SnowflakeDiscord.php @@ -4,11 +4,11 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; +use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeDiscord as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use Ramsey\Identifier\Snowflake\DiscordSnowflakeFactory; -use Ramsey\Identifier\SnowflakeFactory; /** * A Snowflake identifier for use with the Discord voice, text, and streaming video platform @@ -30,7 +30,7 @@ final class SnowflakeDiscord extends BaseSnowflake * @see \Ramsey\Identifier\Snowflake\DiscordSnowflakeFactory::create() */ public function __construct( - string $field, + string $field = 'snowflake', ?string $column = null, private readonly ?int $workerId = null, private readonly ?int $processId = null, @@ -41,6 +41,18 @@ public function __construct( $this->nullable = $nullable; } + #[\Override] + public static function fromInteger( + int|string $identifier, + DatabaseInterface $database, + array $arguments, + ): \Ramsey\Identifier\Snowflake { + return (new DiscordSnowflakeFactory( + $arguments['workerId'], + $arguments['processId'], + ))->createFromInteger($identifier); + } + #[\Override] protected function getListenerClass(): string { @@ -67,8 +79,11 @@ protected function getListenerArgs(): array } #[\Override] - protected function snowflakeFactory(): SnowflakeFactory + protected function getTypecastArgs(): array { - return new DiscordSnowflakeFactory($this->workerId, $this->processId); + return [ + 'workerId' => $this->workerId, + 'processId' => $this->processId, + ]; } } diff --git a/src/SnowflakeGeneric.php b/src/SnowflakeGeneric.php index eca47d7..307db00 100644 --- a/src/SnowflakeGeneric.php +++ b/src/SnowflakeGeneric.php @@ -4,12 +4,12 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; +use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeGeneric as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use Ramsey\Identifier\Snowflake\Epoch; use Ramsey\Identifier\Snowflake\GenericSnowflakeFactory; -use Ramsey\Identifier\SnowflakeFactory; /** * A distributed ID generation system developed by Twitter that produces @@ -32,7 +32,7 @@ final class SnowflakeGeneric extends BaseSnowflake * @see \Ramsey\Identifier\Snowflake\GenericSnowflakeFactory::create() */ public function __construct( - string $field, + string $field = 'snowflake', ?string $column = null, private readonly ?int $node = null, private readonly Epoch|int|null $epochOffset = null, @@ -43,6 +43,18 @@ public function __construct( $this->nullable = $nullable; } + #[\Override] + public static function fromInteger( + int|string $identifier, + DatabaseInterface $database, + array $arguments, + ): \Ramsey\Identifier\Snowflake { + return (new GenericSnowflakeFactory( + $arguments['node'], + $arguments['epochOffset'], + ))->createFromInteger($identifier); + } + #[\Override] protected function getListenerClass(): string { @@ -69,8 +81,11 @@ protected function getListenerArgs(): array } #[\Override] - protected function snowflakeFactory(): SnowflakeFactory + protected function getTypecastArgs(): array { - return new GenericSnowflakeFactory($this->node, $this->epochOffset); + return [ + 'node' => $this->node, + 'epochOffset' => $this->epochOffset, + ]; } } diff --git a/src/SnowflakeInstagram.php b/src/SnowflakeInstagram.php index cc08026..c491292 100644 --- a/src/SnowflakeInstagram.php +++ b/src/SnowflakeInstagram.php @@ -4,11 +4,11 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; +use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeInstagram as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use Ramsey\Identifier\Snowflake\InstagramSnowflakeFactory; -use Ramsey\Identifier\SnowflakeFactory; /** * A Snowflake identifier for use with the Instagram photo and video sharing social media platform @@ -29,7 +29,7 @@ final class SnowflakeInstagram extends BaseSnowflake * @see \Ramsey\Identifier\Snowflake\InstagramSnowflakeFactory::create() */ public function __construct( - string $field, + string $field = 'snowflake', ?string $column = null, private readonly ?int $shardId = null, bool $nullable = false, @@ -39,6 +39,17 @@ public function __construct( $this->nullable = $nullable; } + #[\Override] + public static function fromInteger( + int|string $identifier, + DatabaseInterface $database, + array $arguments, + ): \Ramsey\Identifier\Snowflake { + return (new InstagramSnowflakeFactory( + $arguments['shardId'], + ))->createFromInteger($identifier); + } + #[\Override] protected function getListenerClass(): string { @@ -63,8 +74,10 @@ protected function getListenerArgs(): array } #[\Override] - protected function snowflakeFactory(): SnowflakeFactory + protected function getTypecastArgs(): array { - return new InstagramSnowflakeFactory($this->shardId); + return [ + 'shardId' => $this->shardId, + ]; } } diff --git a/src/SnowflakeMastodon.php b/src/SnowflakeMastodon.php index 6abf0c8..75d50a6 100644 --- a/src/SnowflakeMastodon.php +++ b/src/SnowflakeMastodon.php @@ -4,11 +4,11 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; +use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeMastodon as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use Ramsey\Identifier\Snowflake\MastodonSnowflakeFactory; -use Ramsey\Identifier\SnowflakeFactory; /** * A Snowflake identifier for use with the Mastodon open source platform for decentralized social networking @@ -29,7 +29,7 @@ final class SnowflakeMastodon extends BaseSnowflake * @see \Ramsey\Identifier\Snowflake\MastodonSnowflakeFactory::create() */ public function __construct( - string $field, + string $field = 'snowflake', ?string $column = null, private readonly ?string $tableName = null, bool $nullable = false, @@ -39,6 +39,17 @@ public function __construct( $this->nullable = $nullable; } + #[\Override] + public static function fromInteger( + int|string $identifier, + DatabaseInterface $database, + array $arguments, + ): \Ramsey\Identifier\Snowflake { + return (new MastodonSnowflakeFactory( + $arguments['tableName'], + ))->createFromInteger($identifier); + } + #[\Override] protected function getListenerClass(): string { @@ -63,8 +74,10 @@ protected function getListenerArgs(): array } #[\Override] - protected function snowflakeFactory(): SnowflakeFactory + protected function getTypecastArgs(): array { - return new MastodonSnowflakeFactory($this->tableName); + return [ + 'tableName' => $this->tableName, + ]; } } diff --git a/src/SnowflakeTwitter.php b/src/SnowflakeTwitter.php index f079464..8299a29 100644 --- a/src/SnowflakeTwitter.php +++ b/src/SnowflakeTwitter.php @@ -4,11 +4,11 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; +use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeTwitter as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use Ramsey\Identifier\Snowflake\TwitterSnowflakeFactory; -use Ramsey\Identifier\SnowflakeFactory; /** * A Snowflake identifier for use with the X (formerly Twitter) social media platform @@ -29,7 +29,7 @@ final class SnowflakeTwitter extends BaseSnowflake * @see \Ramsey\Identifier\Snowflake\TwitterSnowflakeFactory::create() */ public function __construct( - string $field, + string $field = 'snowflake', ?string $column = null, private readonly ?int $machineId = null, bool $nullable = false, @@ -39,6 +39,17 @@ public function __construct( $this->nullable = $nullable; } + #[\Override] + public static function fromInteger( + int|string $identifier, + DatabaseInterface $database, + array $arguments, + ): \Ramsey\Identifier\Snowflake { + return (new TwitterSnowflakeFactory( + $arguments['machineId'], + ))->createFromInteger($identifier); + } + #[\Override] protected function getListenerClass(): string { @@ -63,8 +74,10 @@ protected function getListenerArgs(): array } #[\Override] - protected function snowflakeFactory(): SnowflakeFactory + protected function getTypecastArgs(): array { - return new TwitterSnowflakeFactory($this->machineId); + return [ + 'machineId' => $this->machineId, + ]; } } diff --git a/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php index 856f684..ebbb5e6 100644 --- a/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php @@ -29,7 +29,12 @@ abstract class ListenerTest extends BaseTest public function testAssignManually(): void { - $this->withListeners(SnowflakeGenericListener::class); + $this->withListeners([ + SnowflakeGenericListener::class, + [ + 'field' => 'snowflake', + ], + ]); $user = new User(); $user->snowflake = (new GenericSnowflakeFactory(0, 0))->create(); @@ -48,6 +53,7 @@ public function testDiscordSnowflake(): void $this->withListeners([ SnowflakeDiscordListener::class, [ + 'field' => 'snowflake', 'workerId' => 10, 'processId' => 20, ], @@ -90,6 +96,7 @@ public function testGenericSnowflake(): void $this->withListeners([ SnowflakeGenericListener::class, [ + 'field' => 'snowflake', 'node' => 10, 'epochOffset' => 1662744255000, ], @@ -132,6 +139,7 @@ public function testInstagramSnowflake(): void $this->withListeners([ SnowflakeInstagramListener::class, [ + 'field' => 'snowflake', 'shardId' => 10, ], ]); @@ -173,6 +181,7 @@ public function testMastodonSnowflake(): void $this->withListeners([ SnowflakeMastodonListener::class, [ + 'field' => 'snowflake', 'tableName' => 'users', ], ]); @@ -214,6 +223,7 @@ public function testTwitterSnowflake(): void $this->withListeners([ SnowflakeTwitterListener::class, [ + 'field' => 'snowflake', 'machineId' => 10, ], ]); diff --git a/tests/Identifier/Functional/Driver/Common/Snowflake/SnowflakeTest.php b/tests/Identifier/Functional/Driver/Common/Snowflake/SnowflakeTest.php index c5e2103..142db35 100644 --- a/tests/Identifier/Functional/Driver/Common/Snowflake/SnowflakeTest.php +++ b/tests/Identifier/Functional/Driver/Common/Snowflake/SnowflakeTest.php @@ -12,7 +12,6 @@ use Cycle\ORM\Entity\Behavior\Identifier\Tests\Functional\Driver\Common\BaseTest; use Cycle\ORM\Schema\GeneratedField; use Cycle\Schema\Registry; -use Ramsey\Identifier\SnowflakeFactory; use Spiral\Attributes\AttributeReader; use Spiral\Attributes\ReaderInterface; use Spiral\Tokenizer\ClassLocator; @@ -36,8 +35,6 @@ public function testColumnExist(ReaderInterface $reader): void $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()); } @@ -54,8 +51,6 @@ public function testAddColumn(ReaderInterface $reader): void $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()); } @@ -71,36 +66,26 @@ public function testMultipleSnowflake(ReaderInterface $reader): void $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()); } @@ -116,8 +101,6 @@ public function testAddNullableColumn(ReaderInterface $reader): void $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)) diff --git a/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php index 0a5f8cb..6c5f180 100644 --- a/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php @@ -107,7 +107,12 @@ public static function nullableTrueDataProvider(): \Traversable public function testAssignManually(): void { - $this->withListeners(Uuid4Listener::class); + $this->withListeners([ + Uuid4Listener::class, + [ + 'field' => 'uuid', + ], + ]); $user = new User(); $user->uuid = (new UuidFactory())->v4(); @@ -144,6 +149,7 @@ public function testUuid1(): void $this->withListeners([ Uuid1Listener::class, [ + 'field' => 'uuid', 'node' => '00000fffffff', 'clockSeq' => 0xffff, ], @@ -165,6 +171,7 @@ public function testUuid2(): void $this->withListeners([ Uuid2Listener::class, [ + 'field' => 'uuid', 'localDomain' => DceDomain::Person, 'localIdentifier' => 12345678, ], @@ -186,6 +193,7 @@ public function testUuid3(): void $this->withListeners([ Uuid3Listener::class, [ + 'field' => 'uuid', 'namespace' => NamespaceId::Url, 'name' => 'https://example.com/foo', ], @@ -204,7 +212,12 @@ public function testUuid3(): void public function testUuid4(): void { - $this->withListeners(Uuid4Listener::class); + $this->withListeners([ + Uuid4Listener::class, + [ + 'field' => 'uuid', + ], + ]); $user = new User(); $this->save($user); @@ -221,7 +234,11 @@ public function testUuid5(): void { $this->withListeners([ Uuid5Listener::class, - ['namespace' => NamespaceId::Url, 'name' => 'https://example.com/foo'], + [ + 'field' => 'uuid', + 'namespace' => NamespaceId::Url, + 'name' => 'https://example.com/foo', + ], ]); $user = new User(); @@ -237,7 +254,14 @@ public function testUuid5(): void public function testUuid6(): void { - $this->withListeners([Uuid6Listener::class, ['node' => '00000fffffff', 'clockSeq' => 0x1669]]); + $this->withListeners([ + Uuid6Listener::class, + [ + 'field' => 'uuid', + 'node' => '00000fffffff', + 'clockSeq' => 0x1669, + ], + ]); $user = new User(); $this->save($user); @@ -252,7 +276,12 @@ public function testUuid6(): void public function testUuid7(): void { - $this->withListeners(Uuid7Listener::class); + $this->withListeners([ + Uuid7Listener::class, + [ + 'field' => 'uuid', + ], + ]); $user = new User(); $this->save($user); From 907158d1b4f2863d36991e4440de605e0dd0865b Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Wed, 20 Aug 2025 03:52:25 +1000 Subject: [PATCH 18/20] Provides fixes after merged changes broke a lot of things --- src/Listener/Uuid2.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Listener/Uuid2.php b/src/Listener/Uuid2.php index 353c56b..ccac7e3 100644 --- a/src/Listener/Uuid2.php +++ b/src/Listener/Uuid2.php @@ -35,11 +35,12 @@ final class Uuid2 extends BaseUuid public function __construct( string $field, bool $nullable = false, - private readonly DceDomain|int|null $localDomain = null, + private DceDomain|int|null $localDomain = null, private readonly ?int $localIdentifier = null, private readonly int|string|null $node = null, private readonly ?int $clockSeq = null, ) { + $this->localDomain = \is_int($this->localDomain) ? DceDomain::from($this->localDomain) : $this->localDomain; $this->factory = new UuidV2Factory(); parent::__construct($field, $nullable); } From 259a8ba22181b7ee0887a09db8b3dd78cecb42c7 Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 9 Sep 2025 16:40:33 +0400 Subject: [PATCH 19/20] Refactor Snowflace typecasters --- src/Listener/SnowflakeDiscord.php | 4 ++- src/Listener/SnowflakeGeneric.php | 8 ++++++ src/Snowflake.php | 17 +++--------- src/SnowflakeDiscord.php | 38 ++++++++++++-------------- src/SnowflakeGeneric.php | 45 ++++++++++++++++--------------- src/SnowflakeInstagram.php | 36 ++++++++++++------------- src/SnowflakeMastodon.php | 36 ++++++++++++------------- src/SnowflakeTwitter.php | 34 +++++++++++------------ 8 files changed, 107 insertions(+), 111 deletions(-) diff --git a/src/Listener/SnowflakeDiscord.php b/src/Listener/SnowflakeDiscord.php index 86744d7..16dee72 100644 --- a/src/Listener/SnowflakeDiscord.php +++ b/src/Listener/SnowflakeDiscord.php @@ -66,9 +66,11 @@ protected function createValue(): DiscordSnowflake /** * Get the current process ID. + * + * @return int<0, 281474976710655> */ private function getProcessId(): int { - return self::$defaultProcessId === null ? \intval(\getmypid()) : self::$defaultProcessId; + return self::$defaultProcessId ?? \intval(\getmypid()); } } diff --git a/src/Listener/SnowflakeGeneric.php b/src/Listener/SnowflakeGeneric.php index c20343b..170e133 100644 --- a/src/Listener/SnowflakeGeneric.php +++ b/src/Listener/SnowflakeGeneric.php @@ -56,6 +56,14 @@ public static function setDefaults(?int $node, Epoch|int|null $epochOffset): voi } } + /** + * Get default epoch offset. + */ + public static function getEpochOffset(): Epoch|int + { + return self::$epochOffset; + } + #[\Override] protected function createValue(): Snowflake { diff --git a/src/Snowflake.php b/src/Snowflake.php index d01237e..28fcbfe 100644 --- a/src/Snowflake.php +++ b/src/Snowflake.php @@ -4,7 +4,6 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; -use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Schema\BaseModifier; use Cycle\ORM\Entity\Behavior\Schema\RegistryModifier; use Cycle\ORM\Schema\GeneratedField; @@ -24,7 +23,6 @@ abstract class Snowflake extends BaseModifier public function compute(Registry $registry): void { $modifier = new RegistryModifier($registry, $this->role); - /** @var non-empty-string column */ $this->column = $modifier->findColumnName($this->field, $this->column); if (\is_string($this->column) && $this->column !== '') { $modifier->addSnowflakeColumn( @@ -35,7 +33,7 @@ public function compute(Registry $registry): void $modifier->setTypecast( $registry->getEntity($this->role)->getFields()->get($this->field), - [static::class, 'fromInteger', $this->getTypecastArgs()], + $this->getTypecast(), ); } } @@ -44,7 +42,6 @@ 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->addSnowflakeColumn( @@ -55,18 +52,12 @@ public function render(Registry $registry): void $modifier->setTypecast( $registry->getEntity($this->role)->getFields()->get($this->field), - [static::class, 'fromInteger', $this->getTypecastArgs()], + $this->getTypecast(), ); } /** - * @param int<0, max>|numeric-string $identifier + * @return array{0: class-string, 1: non-empty-string, 2?: non-empty-array} */ - abstract public static function fromInteger( - int|string $identifier, - DatabaseInterface $database, - array $arguments, - ): \Ramsey\Identifier\Snowflake; - - abstract protected function getTypecastArgs(): array; + abstract protected function getTypecast(): array; } diff --git a/src/SnowflakeDiscord.php b/src/SnowflakeDiscord.php index 6e7280f..b37302a 100644 --- a/src/SnowflakeDiscord.php +++ b/src/SnowflakeDiscord.php @@ -4,10 +4,10 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; -use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeDiscord as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; +use Ramsey\Identifier\Snowflake\DiscordSnowflake; use Ramsey\Identifier\Snowflake\DiscordSnowflakeFactory; /** @@ -26,8 +26,6 @@ final class SnowflakeDiscord extends BaseSnowflake * @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 * @param bool $nullable Indicates whether to generate a new Snowflake or not - * - * @see \Ramsey\Identifier\Snowflake\DiscordSnowflakeFactory::create() */ public function __construct( string $field = 'snowflake', @@ -41,16 +39,23 @@ public function __construct( $this->nullable = $nullable; } - #[\Override] - public static function fromInteger( + /** + * Identifier factory method from an existing identifier value. + * + * @param int<0, max>|numeric-string $identifier The identifier to create the Snowflake from + * + * @see DiscordSnowflakeFactory::create() + */ + public static function create( int|string $identifier, - DatabaseInterface $database, - array $arguments, - ): \Ramsey\Identifier\Snowflake { - return (new DiscordSnowflakeFactory( - $arguments['workerId'], - $arguments['processId'], - ))->createFromInteger($identifier); + ): DiscordSnowflake { + return new DiscordSnowflake($identifier); + } + + #[\Override] + protected function getTypecast(): array + { + return [self::class, 'create']; } #[\Override] @@ -77,13 +82,4 @@ protected function getListenerArgs(): array 'nullable' => $this->nullable, ]; } - - #[\Override] - protected function getTypecastArgs(): array - { - return [ - 'workerId' => $this->workerId, - 'processId' => $this->processId, - ]; - } } diff --git a/src/SnowflakeGeneric.php b/src/SnowflakeGeneric.php index 307db00..19d0370 100644 --- a/src/SnowflakeGeneric.php +++ b/src/SnowflakeGeneric.php @@ -4,17 +4,19 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; -use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeGeneric as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use Ramsey\Identifier\Snowflake\Epoch; +use Ramsey\Identifier\Snowflake\GenericSnowflake; use Ramsey\Identifier\Snowflake\GenericSnowflakeFactory; /** * A distributed ID generation system developed by Twitter that produces * 64-bit unique, sortable identifiers * + * Use {@see Listener::setDefaults()} to set default node and epoch offset. + * * @Annotation * @NamedArgumentConstructor() * @Target({"CLASS"}) @@ -28,8 +30,6 @@ final class SnowflakeGeneric extends BaseSnowflake * @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 * @param bool $nullable Indicates whether to generate a new Snowflake or not - * - * @see \Ramsey\Identifier\Snowflake\GenericSnowflakeFactory::create() */ public function __construct( string $field = 'snowflake', @@ -43,16 +43,28 @@ public function __construct( $this->nullable = $nullable; } - #[\Override] - public static function fromInteger( + /** + * Identifier factory method from an existing identifier value. + * + * @param int<0, max>|numeric-string $identifier The identifier to create the Snowflake from + * @param int $epochOffset The offset from the Unix Epoch in milliseconds + * + * @see GenericSnowflakeFactory::create() + */ + public static function create( int|string $identifier, - DatabaseInterface $database, - array $arguments, - ): \Ramsey\Identifier\Snowflake { - return (new GenericSnowflakeFactory( - $arguments['node'], - $arguments['epochOffset'], - ))->createFromInteger($identifier); + int $epochOffset, + ): GenericSnowflake { + return new GenericSnowflake($identifier, $epochOffset); + } + + #[\Override] + protected function getTypecast(): array + { + $epochOffset = $this->epochOffset ?? Listener::getEpochOffset(); + $epochOffset instanceof Epoch and $epochOffset = $epochOffset->value; + + return [self::class, 'create', [$epochOffset]]; } #[\Override] @@ -79,13 +91,4 @@ protected function getListenerArgs(): array 'nullable' => $this->nullable, ]; } - - #[\Override] - protected function getTypecastArgs(): array - { - return [ - 'node' => $this->node, - 'epochOffset' => $this->epochOffset, - ]; - } } diff --git a/src/SnowflakeInstagram.php b/src/SnowflakeInstagram.php index c491292..b254b1a 100644 --- a/src/SnowflakeInstagram.php +++ b/src/SnowflakeInstagram.php @@ -4,10 +4,10 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; -use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeInstagram as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; +use Ramsey\Identifier\Snowflake\InstagramSnowflake; use Ramsey\Identifier\Snowflake\InstagramSnowflakeFactory; /** @@ -25,8 +25,6 @@ final class SnowflakeInstagram extends BaseSnowflake * @param non-empty-string|null $column Snowflake column name * @param int<0, 1023>|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 = 'snowflake', @@ -39,15 +37,23 @@ public function __construct( $this->nullable = $nullable; } - #[\Override] - public static function fromInteger( + /** + * Identifier factory method from an existing identifier value. + * + * @param int<0, max>|numeric-string $identifier The identifier to create the Snowflake from + * + * @see InstagramSnowflakeFactory::create() + */ + public static function create( int|string $identifier, - DatabaseInterface $database, - array $arguments, - ): \Ramsey\Identifier\Snowflake { - return (new InstagramSnowflakeFactory( - $arguments['shardId'], - ))->createFromInteger($identifier); + ): InstagramSnowflake { + return new InstagramSnowflake($identifier); + } + + #[\Override] + protected function getTypecast(): array + { + return [self::class, 'create']; } #[\Override] @@ -72,12 +78,4 @@ protected function getListenerArgs(): array 'nullable' => $this->nullable, ]; } - - #[\Override] - protected function getTypecastArgs(): array - { - return [ - 'shardId' => $this->shardId, - ]; - } } diff --git a/src/SnowflakeMastodon.php b/src/SnowflakeMastodon.php index 75d50a6..59231d6 100644 --- a/src/SnowflakeMastodon.php +++ b/src/SnowflakeMastodon.php @@ -4,10 +4,10 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; -use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeMastodon as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; +use Ramsey\Identifier\Snowflake\MastodonSnowflake; use Ramsey\Identifier\Snowflake\MastodonSnowflakeFactory; /** @@ -25,8 +25,6 @@ final class SnowflakeMastodon extends BaseSnowflake * @param non-empty-string|null $column Snowflake column name * @param non-empty-string|null $tableName Database table name ensuring different tables derive separate sequence bases * @param bool $nullable Indicates whether to generate a new Snowflake or not - * - * @see \Ramsey\Identifier\Snowflake\MastodonSnowflakeFactory::create() */ public function __construct( string $field = 'snowflake', @@ -39,15 +37,23 @@ public function __construct( $this->nullable = $nullable; } - #[\Override] - public static function fromInteger( + /** + * Identifier factory method from an existing identifier value. + * + * @param int<0, max>|numeric-string $identifier The identifier to create the Snowflake from + * + * @see MastodonSnowflakeFactory::create() + */ + public static function create( int|string $identifier, - DatabaseInterface $database, - array $arguments, - ): \Ramsey\Identifier\Snowflake { - return (new MastodonSnowflakeFactory( - $arguments['tableName'], - ))->createFromInteger($identifier); + ): MastodonSnowflake { + return new MastodonSnowflake($identifier); + } + + #[\Override] + protected function getTypecast(): array + { + return [self::class, 'create']; } #[\Override] @@ -72,12 +78,4 @@ protected function getListenerArgs(): array 'nullable' => $this->nullable, ]; } - - #[\Override] - protected function getTypecastArgs(): array - { - return [ - 'tableName' => $this->tableName, - ]; - } } diff --git a/src/SnowflakeTwitter.php b/src/SnowflakeTwitter.php index 8299a29..d79a66f 100644 --- a/src/SnowflakeTwitter.php +++ b/src/SnowflakeTwitter.php @@ -4,10 +4,10 @@ namespace Cycle\ORM\Entity\Behavior\Identifier; -use Cycle\Database\DatabaseInterface; use Cycle\ORM\Entity\Behavior\Identifier\Snowflake as BaseSnowflake; use Cycle\ORM\Entity\Behavior\Identifier\Listener\SnowflakeTwitter as Listener; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; +use Ramsey\Identifier\Snowflake\TwitterSnowflake; use Ramsey\Identifier\Snowflake\TwitterSnowflakeFactory; /** @@ -39,15 +39,23 @@ public function __construct( $this->nullable = $nullable; } - #[\Override] - public static function fromInteger( + /** + * Identifier factory method from an existing identifier value. + * + * @param int<0, max>|numeric-string $identifier The identifier to create the Snowflake from + * + * @see TwitterSnowflakeFactory::create() + */ + public static function create( int|string $identifier, - DatabaseInterface $database, - array $arguments, - ): \Ramsey\Identifier\Snowflake { - return (new TwitterSnowflakeFactory( - $arguments['machineId'], - ))->createFromInteger($identifier); + ): TwitterSnowflake { + return new TwitterSnowflake($identifier); + } + + #[\Override] + protected function getTypecast(): array + { + return [self::class, 'create']; } #[\Override] @@ -72,12 +80,4 @@ protected function getListenerArgs(): array 'nullable' => $this->nullable, ]; } - - #[\Override] - protected function getTypecastArgs(): array - { - return [ - 'machineId' => $this->machineId, - ]; - } } From 376a260f5451f320ede6ffd0a686bf0f308cdd1f Mon Sep 17 00:00:00 2001 From: roxblnfk Date: Tue, 9 Sep 2025 16:46:39 +0400 Subject: [PATCH 20/20] Refactor UUID listeners --- src/Listener/Uuid2.php | 7 ++++--- src/Listener/Uuid3.php | 2 +- src/Listener/Uuid5.php | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Listener/Uuid2.php b/src/Listener/Uuid2.php index ccac7e3..098c023 100644 --- a/src/Listener/Uuid2.php +++ b/src/Listener/Uuid2.php @@ -22,7 +22,8 @@ final class Uuid2 extends BaseUuid private static int|string|null $defaultNode = null; private static ?int $defaultClockSeq = null; - private UuidV2Factory $factory; + private readonly UuidV2Factory $factory; + private readonly ?DceDomain $localDomain; /** * @param non-empty-string $field The name of the field to store the UUID @@ -35,12 +36,12 @@ final class Uuid2 extends BaseUuid public function __construct( string $field, bool $nullable = false, - private DceDomain|int|null $localDomain = null, + DceDomain|int|null $localDomain = null, private readonly ?int $localIdentifier = null, private readonly int|string|null $node = null, private readonly ?int $clockSeq = null, ) { - $this->localDomain = \is_int($this->localDomain) ? DceDomain::from($this->localDomain) : $this->localDomain; + $this->localDomain = \is_int($localDomain) ? DceDomain::from($localDomain) : $localDomain; $this->factory = new UuidV2Factory(); parent::__construct($field, $nullable); } diff --git a/src/Listener/Uuid3.php b/src/Listener/Uuid3.php index 40d830b..493d490 100644 --- a/src/Listener/Uuid3.php +++ b/src/Listener/Uuid3.php @@ -14,7 +14,7 @@ */ final class Uuid3 extends Base { - private UuidFactory $factory; + private readonly UuidFactory $factory; /** * @param non-empty-string $field The name of the field to store the UUID diff --git a/src/Listener/Uuid5.php b/src/Listener/Uuid5.php index 7156f28..6de1cac 100644 --- a/src/Listener/Uuid5.php +++ b/src/Listener/Uuid5.php @@ -14,7 +14,7 @@ */ final class Uuid5 extends Base { - private UuidFactory $factory; + private readonly UuidFactory $factory; /** * @param non-empty-string $field The name of the field to store the UUID