From 5c5bf622fbf619d61b9d70d552ca99a21f4f2270 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Wed, 16 Jul 2025 16:50:04 +1000 Subject: [PATCH 01/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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/51] 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 From ea368f814f4f6748fa1a930cc4ea0b8615b79685 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sat, 1 Nov 2025 20:39:32 +1100 Subject: [PATCH 21/51] Update dependencies --- composer.json | 2 +- composer.lock | 1845 ++++++++++--------------------------------------- 2 files changed, 369 insertions(+), 1478 deletions(-) diff --git a/composer.json b/composer.json index 748ed8c..09c2f78 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,7 @@ }, "require": { "php": ">=8.2", - "cycle/entity-behavior": "^1.6", + "cycle/entity-behavior": "^1.7", "ramsey/identifier": "^0.1" }, "require-dev": { diff --git a/composer.lock b/composer.lock index d33972d..7771c10 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "06cc0fca7390f2687878ee8869f101ea", + "content-hash": "1218be50127a6d1fb899d3ee50820934", "packages": [ { "name": "brick/math", @@ -236,16 +236,16 @@ }, { "name": "cycle/orm", - "version": "v2.10.1", + "version": "v2.11.0", "source": { "type": "git", "url": "https://github.com/cycle/orm.git", - "reference": "0b659067c00c3ffbee05109fa17812754acc2525" + "reference": "d712c79eab82a2393863c67c15e37b89fd64b555" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/cycle/orm/zipball/0b659067c00c3ffbee05109fa17812754acc2525", - "reference": "0b659067c00c3ffbee05109fa17812754acc2525", + "url": "https://api.github.com/repos/cycle/orm/zipball/d712c79eab82a2393863c67c15e37b89fd64b555", + "reference": "d712c79eab82a2393863c67c15e37b89fd64b555", "shasum": "" }, "require": { @@ -319,7 +319,7 @@ "type": "github" } ], - "time": "2025-03-31T19:41:17+00:00" + "time": "2025-09-09T09:42:43+00:00" }, { "name": "cycle/schema-builder", @@ -1232,16 +1232,16 @@ }, { "name": "symfony/polyfill-php83", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php83.git", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491" + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/2fb86d65e2d424369ad2905e83b236a8805ba491", - "reference": "2fb86d65e2d424369ad2905e83b236a8805ba491", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", "shasum": "" }, "require": { @@ -1288,7 +1288,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php83/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" }, "funding": [ { @@ -1299,12 +1299,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-07-08T02:45:35+00:00" }, { "name": "yiisoft/friendly-exception", @@ -1440,16 +1444,16 @@ "packages-dev": [ { "name": "amphp/amp", - "version": "v3.1.0", + "version": "v3.1.1", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9" + "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", - "reference": "7cf7fef3d667bfe4b2560bc87e67d5387a7bcde9", + "url": "https://api.github.com/repos/amphp/amp/zipball/fa0ab33a6f47a82929c38d03ca47ebb71086a93f", + "reference": "fa0ab33a6f47a82929c38d03ca47ebb71086a93f", "shasum": "" }, "require": { @@ -1509,7 +1513,7 @@ ], "support": { "issues": "https://github.com/amphp/amp/issues", - "source": "https://github.com/amphp/amp/tree/v3.1.0" + "source": "https://github.com/amphp/amp/tree/v3.1.1" }, "funding": [ { @@ -1517,7 +1521,7 @@ "type": "github" } ], - "time": "2025-01-26T16:07:39+00:00" + "time": "2025-08-27T21:42:00+00:00" }, { "name": "amphp/byte-stream", @@ -1750,16 +1754,16 @@ }, { "name": "amphp/parallel", - "version": "v2.3.1", + "version": "v2.3.2", "source": { "type": "git", "url": "https://github.com/amphp/parallel.git", - "reference": "5113111de02796a782f5d90767455e7391cca190" + "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/5113111de02796a782f5d90767455e7391cca190", - "reference": "5113111de02796a782f5d90767455e7391cca190", + "url": "https://api.github.com/repos/amphp/parallel/zipball/321b45ae771d9c33a068186b24117e3cd1c48dce", + "reference": "321b45ae771d9c33a068186b24117e3cd1c48dce", "shasum": "" }, "require": { @@ -1822,7 +1826,7 @@ ], "support": { "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.3.1" + "source": "https://github.com/amphp/parallel/tree/v2.3.2" }, "funding": [ { @@ -1830,7 +1834,7 @@ "type": "github" } ], - "time": "2024-12-21T01:56:09+00:00" + "time": "2025-08-27T21:55:40+00:00" }, { "name": "amphp/parser", @@ -2246,70 +2250,6 @@ ], "time": "2024-08-03T19:31:26+00:00" }, - { - "name": "clue/ndjson-react", - "version": "v1.3.0", - "source": { - "type": "git", - "url": "https://github.com/clue/reactphp-ndjson.git", - "reference": "392dc165fce93b5bb5c637b67e59619223c931b0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/clue/reactphp-ndjson/zipball/392dc165fce93b5bb5c637b67e59619223c931b0", - "reference": "392dc165fce93b5bb5c637b67e59619223c931b0", - "shasum": "" - }, - "require": { - "php": ">=5.3", - "react/stream": "^1.2" - }, - "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35", - "react/event-loop": "^1.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Clue\\React\\NDJson\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering" - } - ], - "description": "Streaming newline-delimited JSON (NDJSON) parser and encoder for ReactPHP.", - "homepage": "https://github.com/clue/reactphp-ndjson", - "keywords": [ - "NDJSON", - "json", - "jsonlines", - "newline", - "reactphp", - "streaming" - ], - "support": { - "issues": "https://github.com/clue/reactphp-ndjson/issues", - "source": "https://github.com/clue/reactphp-ndjson/tree/v1.3.0" - }, - "funding": [ - { - "url": "https://clue.engineering/support", - "type": "custom" - }, - { - "url": "https://github.com/clue", - "type": "github" - } - ], - "time": "2022-12-23T10:58:28+00:00" - }, { "name": "composer/pcre", "version": "3.3.2", @@ -2391,16 +2331,16 @@ }, { "name": "composer/semver", - "version": "3.4.3", + "version": "3.4.4", "source": { "type": "git", "url": "https://github.com/composer/semver.git", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12" + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/semver/zipball/4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", - "reference": "4313d26ada5e0c4edfbd1dc481a92ff7bff91f12", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", "shasum": "" }, "require": { @@ -2452,7 +2392,7 @@ "support": { "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/semver/issues", - "source": "https://github.com/composer/semver/tree/3.4.3" + "source": "https://github.com/composer/semver/tree/3.4.4" }, "funding": [ { @@ -2462,13 +2402,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-09-19T14:15:21+00:00" + "time": "2025-08-20T19:15:30+00:00" }, { "name": "composer/xdebug-handler", @@ -2817,6 +2753,7 @@ "issues": "https://github.com/doctrine/annotations/issues", "source": "https://github.com/doctrine/annotations/tree/2.0.2" }, + "abandoned": true, "time": "2024-09-05T10:17:24+00:00" }, { @@ -2869,33 +2806,32 @@ }, { "name": "doctrine/inflector", - "version": "2.0.10", + "version": "2.1.0", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc" + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/5817d0659c5b50c9b950feb9af7b9668e2c436bc", - "reference": "5817d0659c5b50c9b950feb9af7b9668e2c436bc", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^11.0", - "phpstan/phpstan": "^1.8", - "phpstan/phpstan-phpunit": "^1.1", - "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^8.5 || ^9.5", - "vimeo/psalm": "^4.25 || ^5.4" + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" }, "type": "library", "autoload": { "psr-4": { - "Doctrine\\Inflector\\": "lib/Doctrine/Inflector" + "Doctrine\\Inflector\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -2940,7 +2876,7 @@ ], "support": { "issues": "https://github.com/doctrine/inflector/issues", - "source": "https://github.com/doctrine/inflector/tree/2.0.10" + "source": "https://github.com/doctrine/inflector/tree/2.1.0" }, "funding": [ { @@ -2956,7 +2892,7 @@ "type": "tidelift" } ], - "time": "2024-02-18T20:23:39+00:00" + "time": "2025-08-10T19:31:58+00:00" }, { "name": "doctrine/lexer", @@ -3035,53 +2971,6 @@ ], "time": "2024-02-05T11:56:58+00:00" }, - { - "name": "evenement/evenement", - "version": "v3.0.2", - "source": { - "type": "git", - "url": "https://github.com/igorw/evenement.git", - "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/igorw/evenement/zipball/0a16b0d71ab13284339abb99d9d2bd813640efbc", - "reference": "0a16b0d71ab13284339abb99d9d2bd813640efbc", - "shasum": "" - }, - "require": { - "php": ">=7.0" - }, - "require-dev": { - "phpunit/phpunit": "^9 || ^6" - }, - "type": "library", - "autoload": { - "psr-4": { - "Evenement\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - } - ], - "description": "Événement is a very simple event dispatching library for PHP", - "keywords": [ - "event-dispatcher", - "event-emitter" - ], - "support": { - "issues": "https://github.com/igorw/evenement/issues", - "source": "https://github.com/igorw/evenement/tree/v3.0.2" - }, - "time": "2023-08-08T05:53:35+00:00" - }, { "name": "felixfbecker/language-server-protocol", "version": "v1.5.3", @@ -3140,16 +3029,16 @@ }, { "name": "fidry/cpu-core-counter", - "version": "1.2.0", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/theofidry/cpu-core-counter.git", - "reference": "8520451a140d3f46ac33042715115e290cf5785f" + "reference": "db9508f7b1474469d9d3c53b86f817e344732678" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", - "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678", + "reference": "db9508f7b1474469d9d3c53b86f817e344732678", "shasum": "" }, "require": { @@ -3159,10 +3048,10 @@ "fidry/makefile": "^0.2.0", "fidry/php-cs-fixer-config": "^1.1.2", "phpstan/extension-installer": "^1.2.0", - "phpstan/phpstan": "^1.9.2", - "phpstan/phpstan-deprecation-rules": "^1.0.0", - "phpstan/phpstan-phpunit": "^1.2.2", - "phpstan/phpstan-strict-rules": "^1.4.4", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-deprecation-rules": "^2.0.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", "phpunit/phpunit": "^8.5.31 || ^9.5.26", "webmozarts/strict-phpunit": "^7.5" }, @@ -3189,7 +3078,7 @@ ], "support": { "issues": "https://github.com/theofidry/cpu-core-counter/issues", - "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0" }, "funding": [ { @@ -3197,112 +3086,7 @@ "type": "github" } ], - "time": "2024-08-06T10:04:20+00:00" - }, - { - "name": "friendsofphp/php-cs-fixer", - "version": "v3.84.0", - "source": { - "type": "git", - "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "38dad0767bf2a9b516b976852200ae722fe984ca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/38dad0767bf2a9b516b976852200ae722fe984ca", - "reference": "38dad0767bf2a9b516b976852200ae722fe984ca", - "shasum": "" - }, - "require": { - "clue/ndjson-react": "^1.0", - "composer/semver": "^3.4", - "composer/xdebug-handler": "^3.0.5", - "ext-filter": "*", - "ext-hash": "*", - "ext-json": "*", - "ext-tokenizer": "*", - "fidry/cpu-core-counter": "^1.2", - "php": "^7.4 || ^8.0", - "react/child-process": "^0.6.6", - "react/event-loop": "^1.0", - "react/promise": "^2.11 || ^3.0", - "react/socket": "^1.0", - "react/stream": "^1.0", - "sebastian/diff": "^4.0.6 || ^5.1.1 || ^6.0.2 || ^7.0", - "symfony/console": "^5.4.45 || ^6.4.13 || ^7.0", - "symfony/event-dispatcher": "^5.4.45 || ^6.4.13 || ^7.0", - "symfony/filesystem": "^5.4.45 || ^6.4.13 || ^7.0", - "symfony/finder": "^5.4.45 || ^6.4.17 || ^7.0", - "symfony/options-resolver": "^5.4.45 || ^6.4.16 || ^7.0", - "symfony/polyfill-mbstring": "^1.32", - "symfony/polyfill-php80": "^1.32", - "symfony/polyfill-php81": "^1.32", - "symfony/process": "^5.4.47 || ^6.4.20 || ^7.2", - "symfony/stopwatch": "^5.4.45 || ^6.4.19 || ^7.0" - }, - "require-dev": { - "facile-it/paraunit": "^1.3.1 || ^2.6", - "infection/infection": "^0.29.14", - "justinrainbow/json-schema": "^5.3 || ^6.4", - "keradus/cli-executor": "^2.2", - "mikey179/vfsstream": "^1.6.12", - "php-coveralls/php-coveralls": "^2.8", - "php-cs-fixer/accessible-object": "^1.1", - "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.6", - "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.6", - "phpunit/phpunit": "^9.6.23 || ^10.5.47 || ^11.5.25", - "symfony/polyfill-php84": "^1.32", - "symfony/var-dumper": "^5.4.48 || ^6.4.23 || ^7.3.1", - "symfony/yaml": "^5.4.45 || ^6.4.23 || ^7.3.1" - }, - "suggest": { - "ext-dom": "For handling output formats in XML", - "ext-mbstring": "For handling non-UTF8 characters." - }, - "bin": [ - "php-cs-fixer" - ], - "type": "application", - "autoload": { - "psr-4": { - "PhpCsFixer\\": "src/" - }, - "exclude-from-classmap": [ - "src/Fixer/Internal/*" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Dariusz Rumiński", - "email": "dariusz.ruminski@gmail.com" - } - ], - "description": "A tool to automatically fix PHP code style", - "keywords": [ - "Static code analysis", - "fixer", - "standards", - "static analysis" - ], - "support": { - "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.84.0" - }, - "funding": [ - { - "url": "https://github.com/keradus", - "type": "github" - } - ], - "time": "2025-07-15T18:21:57+00:00" + "time": "2025-08-14T07:29:31+00:00" }, { "name": "kelunik/certificate", @@ -3538,16 +3322,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.3", + "version": "1.13.4", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", - "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", "shasum": "" }, "require": { @@ -3586,7 +3370,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" }, "funding": [ { @@ -3594,7 +3378,7 @@ "type": "tidelift" } ], - "time": "2025-07-05T12:25:42+00:00" + "time": "2025-08-01T08:46:24+00:00" }, { "name": "netresearch/jsonmapper", @@ -3649,16 +3433,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.5.0", + "version": "v5.6.2", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" + "reference": "3a454ca033b9e06b63282ce19562e892747449bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", - "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", + "reference": "3a454ca033b9e06b63282ce19562e892747449bb", "shasum": "" }, "require": { @@ -3677,7 +3461,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.x-dev" } }, "autoload": { @@ -3701,9 +3485,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" }, - "time": "2025-05-31T08:24:38+00:00" + "time": "2025-10-21T19:32:17+00:00" }, { "name": "phar-io/manifest", @@ -3823,6 +3607,58 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "php-cs-fixer/shim", + "version": "v3.89.1", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/shim.git", + "reference": "69182624902af6b2adafb30e2a092f0e6dd89fab" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/shim/zipball/69182624902af6b2adafb30e2a092f0e6dd89fab", + "reference": "69182624902af6b2adafb30e2a092f0e6dd89fab", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "replace": { + "friendsofphp/php-cs-fixer": "self.version" + }, + "suggest": { + "ext-dom": "For handling output formats in XML", + "ext-mbstring": "For handling non-UTF8 characters." + }, + "bin": [ + "php-cs-fixer", + "php-cs-fixer.phar" + ], + "type": "application", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Dariusz Rumiński", + "email": "dariusz.ruminski@gmail.com" + } + ], + "description": "A tool to automatically fix PHP code style", + "support": { + "issues": "https://github.com/PHP-CS-Fixer/shim/issues", + "source": "https://github.com/PHP-CS-Fixer/shim/tree/v3.89.1" + }, + "time": "2025-10-24T12:05:38+00:00" + }, { "name": "phpdocumentor/reflection-common", "version": "2.2.0", @@ -3878,16 +3714,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "5.6.2", + "version": "5.6.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" + "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", - "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/94f8051919d1b0369a6bcc7931d679a511c03fe9", + "reference": "94f8051919d1b0369a6bcc7931d679a511c03fe9", "shasum": "" }, "require": { @@ -3936,9 +3772,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.3" }, - "time": "2025-04-13T19:20:35+00:00" + "time": "2025-08-01T19:43:32+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -4000,16 +3836,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", - "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", "shasum": "" }, "require": { @@ -4041,9 +3877,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" }, - "time": "2025-07-13T07:04:09+00:00" + "time": "2025-08-30T15:50:23+00:00" }, { "name": "phpunit/php-code-coverage", @@ -4366,16 +4202,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.23", + "version": "9.6.29", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95" + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", - "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", "shasum": "" }, "require": { @@ -4386,7 +4222,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.1", + "myclabs/deep-copy": "^1.13.4", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=7.3", @@ -4397,11 +4233,11 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", + "sebastian/comparator": "^4.0.9", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", + "sebastian/exporter": "^4.0.8", + "sebastian/global-state": "^5.0.8", "sebastian/object-enumerator": "^4.0.4", "sebastian/resource-operations": "^3.0.4", "sebastian/type": "^3.2.1", @@ -4449,7 +4285,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" }, "funding": [ { @@ -4473,7 +4309,7 @@ "type": "tidelift" } ], - "time": "2025-05-02T06:40:34+00:00" + "time": "2025-09-24T06:29:11+00:00" }, { "name": "psr/cache", @@ -4633,30 +4469,37 @@ "time": "2023-04-04T09:54:51+00:00" }, { - "name": "react/cache", - "version": "v1.2.0", + "name": "revolt/event-loop", + "version": "v1.0.7", "source": { "type": "git", - "url": "https://github.com/reactphp/cache.git", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b" + "url": "https://github.com/revoltphp/event-loop.git", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/cache/zipball/d47c472b64aa5608225f47965a484b75c7817d5b", - "reference": "d47c472b64aa5608225f47965a484b75c7817d5b", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", + "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", "shasum": "" }, "require": { - "php": ">=5.3.0", - "react/promise": "^3.0 || ^2.0 || ^1.1" + "php": ">=8.1" }, "require-dev": { - "phpunit/phpunit": "^9.5 || ^5.7 || ^4.8.35" + "ext-json": "*", + "jetbrains/phpstorm-stubs": "^2019.3", + "phpunit/phpunit": "^9", + "psalm/phar": "^5.15" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + } + }, "autoload": { "psr-4": { - "React\\Cache\\": "src/" + "Revolt\\": "src" } }, "notification-url": "https://packagist.org/downloads/", @@ -4665,605 +4508,72 @@ ], "authors": [ { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" }, { "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" + "email": "ceesjank@gmail.com" }, { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" + "name": "Christian Lück", + "email": "christian@clue.engineering" }, { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" + "name": "Niklas Keller", + "email": "me@kelunik.com" } ], - "description": "Async, Promise-based cache interface for ReactPHP", + "description": "Rock-solid event loop for concurrent PHP applications.", "keywords": [ - "cache", - "caching", - "promise", - "reactphp" + "async", + "asynchronous", + "concurrency", + "event", + "event-loop", + "non-blocking", + "scheduler" ], "support": { - "issues": "https://github.com/reactphp/cache/issues", - "source": "https://github.com/reactphp/cache/tree/v1.2.0" + "issues": "https://github.com/revoltphp/event-loop/issues", + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2022-11-30T15:59:55+00:00" + "time": "2025-01-25T19:27:39+00:00" }, { - "name": "react/child-process", - "version": "v0.6.6", + "name": "sebastian/cli-parser", + "version": "1.0.2", "source": { "type": "git", - "url": "https://github.com/reactphp/child-process.git", - "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159" + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/reactphp/child-process/zipball/1721e2b93d89b745664353b9cfc8f155ba8a6159", - "reference": "1721e2b93d89b745664353b9cfc8f155ba8a6159", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", "shasum": "" }, "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/event-loop": "^1.2", - "react/stream": "^1.4" + "php": ">=7.3" }, "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/socket": "^1.16", - "sebastian/environment": "^5.0 || ^3.0 || ^2.0 || ^1.0" + "phpunit/phpunit": "^9.3" }, "type": "library", - "autoload": { - "psr-4": { - "React\\ChildProcess\\": "src/" + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" } }, + "autoload": { + "classmap": [ + "src/" + ] + }, "notification-url": "https://packagist.org/downloads/", "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Event-driven library for executing child processes with ReactPHP.", - "keywords": [ - "event-driven", - "process", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/child-process/issues", - "source": "https://github.com/reactphp/child-process/tree/v0.6.6" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2025-01-01T16:37:48+00:00" - }, - { - "name": "react/dns", - "version": "v1.13.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/dns.git", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/dns/zipball/eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", - "reference": "eb8ae001b5a455665c89c1df97f6fb682f8fb0f5", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "react/cache": "^1.0 || ^0.6 || ^0.5", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.7 || ^1.2.1" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3 || ^2", - "react/promise-timer": "^1.11" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Dns\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async DNS resolver for ReactPHP", - "keywords": [ - "async", - "dns", - "dns-resolver", - "reactphp" - ], - "support": { - "issues": "https://github.com/reactphp/dns/issues", - "source": "https://github.com/reactphp/dns/tree/v1.13.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2024-06-13T14:18:03+00:00" - }, - { - "name": "react/event-loop", - "version": "v1.5.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/event-loop.git", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/event-loop/zipball/bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "reference": "bbe0bd8c51ffc05ee43f1729087ed3bdf7d53354", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "suggest": { - "ext-pcntl": "For signal handling support when using the StreamSelectLoop" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\EventLoop\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "ReactPHP's core reactor event loop that libraries can use for evented I/O.", - "keywords": [ - "asynchronous", - "event-loop" - ], - "support": { - "issues": "https://github.com/reactphp/event-loop/issues", - "source": "https://github.com/reactphp/event-loop/tree/v1.5.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2023-11-13T13:48:05+00:00" - }, - { - "name": "react/promise", - "version": "v3.2.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/promise.git", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/promise/zipball/8a164643313c71354582dc850b42b33fa12a4b63", - "reference": "8a164643313c71354582dc850b42b33fa12a4b63", - "shasum": "" - }, - "require": { - "php": ">=7.1.0" - }, - "require-dev": { - "phpstan/phpstan": "1.10.39 || 1.4.10", - "phpunit/phpunit": "^9.6 || ^7.5" - }, - "type": "library", - "autoload": { - "files": [ - "src/functions_include.php" - ], - "psr-4": { - "React\\Promise\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "A lightweight implementation of CommonJS Promises/A for PHP", - "keywords": [ - "promise", - "promises" - ], - "support": { - "issues": "https://github.com/reactphp/promise/issues", - "source": "https://github.com/reactphp/promise/tree/v3.2.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2024-05-24T10:39:05+00:00" - }, - { - "name": "react/socket", - "version": "v1.16.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/socket.git", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/socket/zipball/23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "reference": "23e4ff33ea3e160d2d1f59a0e6050e4b0fb0eac1", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.0", - "react/dns": "^1.13", - "react/event-loop": "^1.2", - "react/promise": "^3.2 || ^2.6 || ^1.2.1", - "react/stream": "^1.4" - }, - "require-dev": { - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36", - "react/async": "^4.3 || ^3.3 || ^2", - "react/promise-stream": "^1.4", - "react/promise-timer": "^1.11" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Socket\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Async, streaming plaintext TCP/IP and secure TLS socket server and client connections for ReactPHP", - "keywords": [ - "Connection", - "Socket", - "async", - "reactphp", - "stream" - ], - "support": { - "issues": "https://github.com/reactphp/socket/issues", - "source": "https://github.com/reactphp/socket/tree/v1.16.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2024-07-26T10:38:09+00:00" - }, - { - "name": "react/stream", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/reactphp/stream.git", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/reactphp/stream/zipball/1e5b0acb8fe55143b5b426817155190eb6f5b18d", - "reference": "1e5b0acb8fe55143b5b426817155190eb6f5b18d", - "shasum": "" - }, - "require": { - "evenement/evenement": "^3.0 || ^2.0 || ^1.0", - "php": ">=5.3.8", - "react/event-loop": "^1.2" - }, - "require-dev": { - "clue/stream-filter": "~1.2", - "phpunit/phpunit": "^9.6 || ^5.7 || ^4.8.36" - }, - "type": "library", - "autoload": { - "psr-4": { - "React\\Stream\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Christian Lück", - "email": "christian@clue.engineering", - "homepage": "https://clue.engineering/" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "reactphp@ceesjankiewiet.nl", - "homepage": "https://wyrihaximus.net/" - }, - { - "name": "Jan Sorgalla", - "email": "jsorgalla@gmail.com", - "homepage": "https://sorgalla.com/" - }, - { - "name": "Chris Boden", - "email": "cboden@gmail.com", - "homepage": "https://cboden.dev/" - } - ], - "description": "Event-driven readable and writable streams for non-blocking I/O in ReactPHP", - "keywords": [ - "event-driven", - "io", - "non-blocking", - "pipe", - "reactphp", - "readable", - "stream", - "writable" - ], - "support": { - "issues": "https://github.com/reactphp/stream/issues", - "source": "https://github.com/reactphp/stream/tree/v1.4.0" - }, - "funding": [ - { - "url": "https://opencollective.com/reactphp", - "type": "open_collective" - } - ], - "time": "2024-06-11T12:45:25+00:00" - }, - { - "name": "revolt/event-loop", - "version": "v1.0.7", - "source": { - "type": "git", - "url": "https://github.com/revoltphp/event-loop.git", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/09bf1bf7f7f574453efe43044b06fafe12216eb3", - "reference": "09bf1bf7f7f574453efe43044b06fafe12216eb3", - "shasum": "" - }, - "require": { - "php": ">=8.1" - }, - "require-dev": { - "ext-json": "*", - "jetbrains/phpstorm-stubs": "^2019.3", - "phpunit/phpunit": "^9", - "psalm/phar": "^5.15" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Revolt\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Aaron Piotrowski", - "email": "aaron@trowski.com" - }, - { - "name": "Cees-Jan Kiewiet", - "email": "ceesjank@gmail.com" - }, - { - "name": "Christian Lück", - "email": "christian@clue.engineering" - }, - { - "name": "Niklas Keller", - "email": "me@kelunik.com" - } - ], - "description": "Rock-solid event loop for concurrent PHP applications.", - "keywords": [ - "async", - "asynchronous", - "concurrency", - "event", - "event-loop", - "non-blocking", - "scheduler" - ], - "support": { - "issues": "https://github.com/revoltphp/event-loop/issues", - "source": "https://github.com/revoltphp/event-loop/tree/v1.0.7" - }, - "time": "2025-01-25T19:27:39+00:00" - }, - { - "name": "sebastian/cli-parser", - "version": "1.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" + "BSD-3-Clause" ], "authors": [ { @@ -5399,16 +4709,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "4.0.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", "shasum": "" }, "require": { @@ -5461,15 +4771,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2025-08-10T06:51:50+00:00" }, { "name": "sebastian/complexity", @@ -5659,16 +4981,16 @@ }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "4.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/14c6ba52f95a36c3d27c835d65efc7123c446e8c", + "reference": "14c6ba52f95a36c3d27c835d65efc7123c446e8c", "shasum": "" }, "require": { @@ -5724,28 +5046,40 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2025-09-24T06:03:27+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "5.0.8", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b6781316bdcd28260904e7cc18ec983d0d2ef4f6", + "reference": "b6781316bdcd28260904e7cc18ec983d0d2ef4f6", "shasum": "" }, "require": { @@ -5788,15 +5122,27 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.8" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2025-08-10T07:10:35+00:00" }, { "name": "sebastian/lines-of-code", @@ -5969,16 +5315,16 @@ }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "4.0.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/539c6691e0623af6dc6f9c20384c120f963465a0", + "reference": "539c6691e0623af6dc6f9c20384c120f963465a0", "shasum": "" }, "require": { @@ -6020,15 +5366,27 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.6" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2025-08-10T06:57:39+00:00" }, { "name": "sebastian/resource-operations", @@ -6345,21 +5703,21 @@ }, { "name": "spiral/code-style", - "version": "v2.2.2", + "version": "v2.3.1", "source": { "type": "git", "url": "https://github.com/spiral/code-style.git", - "reference": "3803c38baf6cda714e9ebbc7e515622b22ea798d" + "reference": "a0407ffcb300c9d2977cb3d1c36af0d13b2ffb72" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spiral/code-style/zipball/3803c38baf6cda714e9ebbc7e515622b22ea798d", - "reference": "3803c38baf6cda714e9ebbc7e515622b22ea798d", + "url": "https://api.github.com/repos/spiral/code-style/zipball/a0407ffcb300c9d2977cb3d1c36af0d13b2ffb72", + "reference": "a0407ffcb300c9d2977cb3d1c36af0d13b2ffb72", "shasum": "" }, "require": { - "friendsofphp/php-cs-fixer": "^3.64", - "php": ">=8.0" + "php": ">=8.0", + "php-cs-fixer/shim": "^3.64" }, "require-dev": { "phpunit/phpunit": "^10.5", @@ -6389,7 +5747,7 @@ "description": "Code style and static analysis tools rulesets collection", "homepage": "https://github.com/spiral/code-style", "support": { - "source": "https://github.com/spiral/code-style/tree/v2.2.2" + "source": "https://github.com/spiral/code-style/tree/v2.3.1" }, "funding": [ { @@ -6397,7 +5755,7 @@ "type": "github" } ], - "time": "2025-01-24T07:31:21+00:00" + "time": "2025-10-06T17:24:48+00:00" }, { "name": "spiral/logger", @@ -6548,16 +5906,16 @@ }, { "name": "symfony/console", - "version": "v7.3.1", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101" + "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/9e27aecde8f506ba0fd1d9989620c04a87697101", - "reference": "9e27aecde8f506ba0fd1d9989620c04a87697101", + "url": "https://api.github.com/repos/symfony/console/zipball/cdb80fa5869653c83cfe1a9084a673b6daf57ea7", + "reference": "cdb80fa5869653c83cfe1a9084a673b6daf57ea7", "shasum": "" }, "require": { @@ -6622,7 +5980,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.3.1" + "source": "https://github.com/symfony/console/tree/v7.3.5" }, "funding": [ { @@ -6633,12 +5991,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-06-27T19:55:54+00:00" + "time": "2025-10-14T15:46:26+00:00" }, { "name": "symfony/deprecation-contracts", @@ -6707,174 +6069,18 @@ ], "time": "2024-09-25T14:21:43+00:00" }, - { - "name": "symfony/event-dispatcher", - "version": "v7.3.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/497f73ac996a598c92409b44ac43b6690c4f666d", - "reference": "497f73ac996a598c92409b44ac43b6690c4f666d", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/event-dispatcher-contracts": "^2.5|^3" - }, - "conflict": { - "symfony/dependency-injection": "<6.4", - "symfony/service-contracts": "<2.5" - }, - "provide": { - "psr/event-dispatcher-implementation": "1.0", - "symfony/event-dispatcher-implementation": "2.0|3.0" - }, - "require-dev": { - "psr/log": "^1|^2|^3", - "symfony/config": "^6.4|^7.0", - "symfony/dependency-injection": "^6.4|^7.0", - "symfony/error-handler": "^6.4|^7.0", - "symfony/expression-language": "^6.4|^7.0", - "symfony/http-foundation": "^6.4|^7.0", - "symfony/service-contracts": "^2.5|^3", - "symfony/stopwatch": "^6.4|^7.0" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\EventDispatcher\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v7.3.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-04-22T09:11:45+00:00" - }, - { - "name": "symfony/event-dispatcher-contracts", - "version": "v3.6.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586", - "shasum": "" - }, - "require": { - "php": ">=8.1", - "psr/event-dispatcher": "^1" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/contracts", - "name": "symfony/contracts" - }, - "branch-alias": { - "dev-main": "3.6-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Contracts\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Generic abstractions related to dispatching event", - "homepage": "https://symfony.com", - "keywords": [ - "abstractions", - "contracts", - "decoupling", - "interfaces", - "interoperability", - "standards" - ], - "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-09-25T14:21:43+00:00" - }, { "name": "symfony/filesystem", - "version": "v7.3.0", + "version": "v7.3.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb" + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/b8dce482de9d7c9fe2891155035a7248ab5c7fdb", - "reference": "b8dce482de9d7c9fe2891155035a7248ab5c7fdb", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/edcbb768a186b5c3f25d0643159a787d3e63b7fd", + "reference": "edcbb768a186b5c3f25d0643159a787d3e63b7fd", "shasum": "" }, "require": { @@ -6911,7 +6117,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.3.0" + "source": "https://github.com/symfony/filesystem/tree/v7.3.2" }, "funding": [ { @@ -6922,25 +6128,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-10-25T15:15:23+00:00" + "time": "2025-07-07T08:17:47+00:00" }, { "name": "symfony/finder", - "version": "v7.3.0", + "version": "v7.3.5", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + "reference": "9f696d2f1e340484b4683f7853b273abff94421f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", - "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "url": "https://api.github.com/repos/symfony/finder/zipball/9f696d2f1e340484b4683f7853b273abff94421f", + "reference": "9f696d2f1e340484b4683f7853b273abff94421f", "shasum": "" }, "require": { @@ -6972,77 +6182,10 @@ "homepage": "https://symfony.com/contributors" } ], - "description": "Finds files and directories via an intuitive fluent interface", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/finder/tree/v7.3.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-12-30T19:00:26+00:00" - }, - { - "name": "symfony/options-resolver", - "version": "v7.3.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/options-resolver.git", - "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/options-resolver/zipball/afb9a8038025e5dbc657378bfab9198d75f10fca", - "reference": "afb9a8038025e5dbc657378bfab9198d75f10fca", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/deprecation-contracts": "^2.5|^3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\OptionsResolver\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides an improved replacement for the array_replace PHP function", - "homepage": "https://symfony.com", - "keywords": [ - "config", - "configuration", - "options" - ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/options-resolver/tree/v7.3.0" + "source": "https://github.com/symfony/finder/tree/v7.3.5" }, "funding": [ { @@ -7053,16 +6196,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-04T13:12:05+00:00" + "time": "2025-10-15T18:45:57+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -7121,7 +6268,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" }, "funding": [ { @@ -7132,6 +6279,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -7141,16 +6292,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", - "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", + "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", "shasum": "" }, "require": { @@ -7199,7 +6350,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" }, "funding": [ { @@ -7210,16 +6361,20 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-06-27T09:58:17+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -7280,7 +6435,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" }, "funding": [ { @@ -7291,6 +6446,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -7300,7 +6459,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -7361,87 +6520,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2024-12-23T08:48:59+00:00" - }, - { - "name": "symfony/polyfill-php80", - "version": "v1.32.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Ion Bazan", - "email": "ion.bazan@gmail.com" - }, - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" }, "funding": [ { @@ -7453,79 +6532,7 @@ "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-01-02T08:10:11+00:00" - }, - { - "name": "symfony/polyfill-php81", - "version": "v1.32.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php81.git", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "reference": "4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c", - "shasum": "" - }, - "require": { - "php": ">=7.2" - }, - "type": "library", - "extra": { - "thanks": { - "url": "https://github.com/symfony/polyfill", - "name": "symfony/polyfill" - } - }, - "autoload": { - "files": [ - "bootstrap.php" - ], - "psr-4": { - "Symfony\\Polyfill\\Php81\\": "" - }, - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "support": { - "source": "https://github.com/symfony/polyfill-php81/tree/v1.32.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/nicolas-grekas", "type": "github" }, { @@ -7533,20 +6540,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.32.0", + "version": "v1.33.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "000df7860439609837bbe28670b0be15783b7fbf" + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/000df7860439609837bbe28670b0be15783b7fbf", - "reference": "000df7860439609837bbe28670b0be15783b7fbf", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", + "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", "shasum": "" }, "require": { @@ -7593,7 +6600,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" }, "funding": [ { @@ -7605,64 +6612,7 @@ "type": "github" }, { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-02-20T12:04:08+00:00" - }, - { - "name": "symfony/process", - "version": "v7.3.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", - "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", - "shasum": "" - }, - "require": { - "php": ">=8.2" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Process\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Executes commands in sub-processes", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/process/tree/v7.3.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", + "url": "https://github.com/nicolas-grekas", "type": "github" }, { @@ -7670,7 +6620,7 @@ "type": "tidelift" } ], - "time": "2025-04-17T09:11:12+00:00" + "time": "2025-06-24T13:30:11+00:00" }, { "name": "symfony/service-contracts", @@ -7755,80 +6705,18 @@ ], "time": "2025-04-25T09:37:31+00:00" }, - { - "name": "symfony/stopwatch", - "version": "v7.3.0", - "source": { - "type": "git", - "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", - "reference": "5a49289e2b308214c8b9c2fda4ea454d8b8ad7cd", - "shasum": "" - }, - "require": { - "php": ">=8.2", - "symfony/service-contracts": "^2.5|^3" - }, - "type": "library", - "autoload": { - "psr-4": { - "Symfony\\Component\\Stopwatch\\": "" - }, - "exclude-from-classmap": [ - "/Tests/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Provides a way to profile code", - "homepage": "https://symfony.com", - "support": { - "source": "https://github.com/symfony/stopwatch/tree/v7.3.0" - }, - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2025-02-24T10:49:57+00:00" - }, { "name": "symfony/string", - "version": "v7.3.0", + "version": "v7.3.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" + "reference": "f96476035142921000338bad71e5247fbc138872" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", - "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", + "url": "https://api.github.com/repos/symfony/string/zipball/f96476035142921000338bad71e5247fbc138872", + "reference": "f96476035142921000338bad71e5247fbc138872", "shasum": "" }, "require": { @@ -7843,7 +6731,6 @@ }, "require-dev": { "symfony/emoji": "^7.1", - "symfony/error-handler": "^6.4|^7.0", "symfony/http-client": "^6.4|^7.0", "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", @@ -7886,7 +6773,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.3.0" + "source": "https://github.com/symfony/string/tree/v7.3.4" }, "funding": [ { @@ -7897,12 +6784,16 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2025-04-20T20:19:01+00:00" + "time": "2025-09-11T14:36:48+00:00" }, { "name": "theseer/tokenizer", @@ -7956,16 +6847,16 @@ }, { "name": "vimeo/psalm", - "version": "6.13.0", + "version": "6.13.1", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "70cdf647255a1362b426bb0f522a85817b8c791c" + "reference": "1e3b7f0a8ab32b23197b91107adc0a7ed8a05b51" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/70cdf647255a1362b426bb0f522a85817b8c791c", - "reference": "70cdf647255a1362b426bb0f522a85817b8c791c", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/1e3b7f0a8ab32b23197b91107adc0a7ed8a05b51", + "reference": "1e3b7f0a8ab32b23197b91107adc0a7ed8a05b51", "shasum": "" }, "require": { @@ -8070,32 +6961,32 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2025-07-14T09:59:17+00:00" + "time": "2025-08-06T10:10:28+00:00" }, { "name": "webmozart/assert", - "version": "1.11.0", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", - "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9be6926d8b485f55b9229203f962b51ed377ba68", + "reference": "9be6926d8b485f55b9229203f962b51ed377ba68", "shasum": "" }, "require": { "ext-ctype": "*", + "ext-date": "*", + "ext-filter": "*", "php": "^7.2 || ^8.0" }, - "conflict": { - "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<4.6.1 || 4.6.2" - }, - "require-dev": { - "phpunit/phpunit": "^8.5.13" + "suggest": { + "ext-intl": "", + "ext-simplexml": "", + "ext-spl": "" }, "type": "library", "extra": { @@ -8126,9 +7017,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.11.0" + "source": "https://github.com/webmozarts/assert/tree/1.12.1" }, - "time": "2022-06-03T18:03:27+00:00" + "time": "2025-10-29T15:56:20+00:00" } ], "aliases": [], From e0b77a0b45aef5cdf86d19e1a30b5d9b606ac49f Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sat, 1 Nov 2025 20:41:17 +1100 Subject: [PATCH 22/51] Update docblock description for each Snowflake identifier --- src/SnowflakeDiscord.php | 2 +- src/SnowflakeGeneric.php | 2 +- src/SnowflakeInstagram.php | 2 +- src/SnowflakeMastodon.php | 2 +- src/SnowflakeTwitter.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/SnowflakeDiscord.php b/src/SnowflakeDiscord.php index b37302a..77ef82f 100644 --- a/src/SnowflakeDiscord.php +++ b/src/SnowflakeDiscord.php @@ -40,7 +40,7 @@ public function __construct( } /** - * Identifier factory method from an existing identifier value. + * Create a new Discord Snowflake instance from an existing identifier value. * * @param int<0, max>|numeric-string $identifier The identifier to create the Snowflake from * diff --git a/src/SnowflakeGeneric.php b/src/SnowflakeGeneric.php index 19d0370..17d19de 100644 --- a/src/SnowflakeGeneric.php +++ b/src/SnowflakeGeneric.php @@ -44,7 +44,7 @@ public function __construct( } /** - * Identifier factory method from an existing identifier value. + * Create a new Generic Snowflake instance 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 diff --git a/src/SnowflakeInstagram.php b/src/SnowflakeInstagram.php index b254b1a..b0623a2 100644 --- a/src/SnowflakeInstagram.php +++ b/src/SnowflakeInstagram.php @@ -38,7 +38,7 @@ public function __construct( } /** - * Identifier factory method from an existing identifier value. + * Create a new Instagram Snowflake instance from an existing identifier value. * * @param int<0, max>|numeric-string $identifier The identifier to create the Snowflake from * diff --git a/src/SnowflakeMastodon.php b/src/SnowflakeMastodon.php index 59231d6..031fbac 100644 --- a/src/SnowflakeMastodon.php +++ b/src/SnowflakeMastodon.php @@ -38,7 +38,7 @@ public function __construct( } /** - * Identifier factory method from an existing identifier value. + * Create a new Mastodon Snowflake instance from an existing identifier value. * * @param int<0, max>|numeric-string $identifier The identifier to create the Snowflake from * diff --git a/src/SnowflakeTwitter.php b/src/SnowflakeTwitter.php index d79a66f..1c3f177 100644 --- a/src/SnowflakeTwitter.php +++ b/src/SnowflakeTwitter.php @@ -40,7 +40,7 @@ public function __construct( } /** - * Identifier factory method from an existing identifier value. + * Create a new Twitter Snowflake instance from an existing identifier value. * * @param int<0, max>|numeric-string $identifier The identifier to create the Snowflake from * From 70611d2792ca2d29869e8dc420dea614ba2f5477 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sat, 1 Nov 2025 22:43:23 +1100 Subject: [PATCH 23/51] Updated Ulid following a consistent structure --- src/Listener/Ulid.php | 8 +++-- src/Ulid.php | 33 +++++++++++-------- .../Driver/Common/Ulid/ListenerTest.php | 4 +-- .../Driver/Common/Ulid/UlidTest.php | 12 +++---- 4 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/Listener/Ulid.php b/src/Listener/Ulid.php index 763b3bf..df8e417 100644 --- a/src/Listener/Ulid.php +++ b/src/Listener/Ulid.php @@ -6,6 +6,7 @@ use Cycle\ORM\Entity\Behavior\Attribute\Listen; use Cycle\ORM\Entity\Behavior\Event\Mapper\Command\OnCreate; +use Ramsey\Identifier\Ulid\Ulid as UlidIdentifier; use Ramsey\Identifier\Ulid\UlidFactory; final class Ulid @@ -22,8 +23,11 @@ public function __invoke(OnCreate $event): void return; } - $identifier = (new UlidFactory())->create(); + $event->state->register($this->field, $this->createValue()); + } - $event->state->register($this->field, $identifier); + protected function createValue(): UlidIdentifier + { + return (new UlidFactory())->create(); } } diff --git a/src/Ulid.php b/src/Ulid.php index d54761c..7a10077 100644 --- a/src/Ulid.php +++ b/src/Ulid.php @@ -13,7 +13,7 @@ use JetBrains\PhpStorm\ArrayShape; use Ramsey\Identifier\Ulid\MaxUlid; use Ramsey\Identifier\Ulid\NilUlid; -use Ramsey\Identifier\Ulid\Ulid as UlidInterface; +use Ramsey\Identifier\Ulid\Ulid as UlidIdentifier; use Ramsey\Identifier\Ulid\UlidFactory; /** @@ -30,8 +30,6 @@ final class Ulid extends BaseModifier * @param non-empty-string $field Ulid property name * @param string|null $column Ulid column name * @param bool $nullable Indicates whether to generate a new Ulid or not - * - * @see \Ramsey\Identifier\Ulid\UlidFactory::create() */ public function __construct( private string $field = 'ulid', @@ -39,14 +37,6 @@ public function __construct( private bool $nullable = false, ) {} - /** - * @param non-empty-string $value - */ - public static function fromString(string $value): MaxUlid|NilUlid|UlidInterface - { - return (new UlidFactory())->createFromString($value); - } - #[\Override] public function compute(Registry $registry): void { @@ -61,7 +51,7 @@ public function compute(Registry $registry): void $modifier->setTypecast( $registry->getEntity($this->role)->getFields()->get($this->field), - [self::class, 'fromString'], + $this->getTypecast(), ); } } @@ -81,10 +71,27 @@ public function render(Registry $registry): void $modifier->setTypecast( $registry->getEntity($this->role)->getFields()->get($this->field), - [self::class, 'fromString'], + $this->getTypecast(), ); } + /** + * Create a new Ulid instance from an existing identifier value. + * + * @param non-empty-string $identifier The identifier to create the Ulid from + * + * @see UlidFactory::create() + */ + public static function create(string $identifier): MaxUlid|NilUlid|UlidIdentifier + { + return (new UlidFactory())->createFromString($identifier); + } + + protected function getTypecast(): array + { + return [self::class, 'create']; + } + #[\Override] protected function getListenerClass(): string { diff --git a/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php index 1e29ed9..6764f8f 100644 --- a/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php @@ -85,8 +85,8 @@ public function withListeners(array|string $listeners): void SchemaInterface::SCHEMA => [], SchemaInterface::RELATIONS => [], SchemaInterface::TYPECAST => [ - 'ulid' => [Ulid::class, 'fromString'], - 'foo_ulid' => [Ulid::class, 'fromString'], + 'ulid' => [Ulid::class, 'create'], + 'foo_ulid' => [Ulid::class, 'create'], ], ], ])); diff --git a/tests/Identifier/Functional/Driver/Common/Ulid/UlidTest.php b/tests/Identifier/Functional/Driver/Common/Ulid/UlidTest.php index 2388e84..6938383 100644 --- a/tests/Identifier/Functional/Driver/Common/Ulid/UlidTest.php +++ b/tests/Identifier/Functional/Driver/Common/Ulid/UlidTest.php @@ -35,7 +35,7 @@ public function testColumnExist(ReaderInterface $reader): void $this->assertTrue($fields->has('ulid')); $this->assertTrue($fields->hasColumn('ulid')); $this->assertSame('ulid', $fields->get('ulid')->getType()); - $this->assertSame([Ulid::class, 'fromString'], $fields->get('ulid')->getTypecast()); + $this->assertSame([Ulid::class, 'create'], $fields->get('ulid')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('ulid')->getGenerated()); $this->assertSame(1, $fields->count()); } @@ -52,7 +52,7 @@ public function testAddColumn(ReaderInterface $reader): void $this->assertTrue($fields->has('customUlid')); $this->assertTrue($fields->hasColumn('custom_ulid')); $this->assertSame('ulid', $fields->get('customUlid')->getType()); - $this->assertSame([Ulid::class, 'fromString'], $fields->get('customUlid')->getTypecast()); + $this->assertSame([Ulid::class, 'create'], $fields->get('customUlid')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('customUlid')->getGenerated()); } @@ -68,19 +68,19 @@ public function testMultipleUlid(ReaderInterface $reader): void $this->assertTrue($fields->has('ulid')); $this->assertTrue($fields->hasColumn('ulid')); $this->assertSame('ulid', $fields->get('ulid')->getType()); - $this->assertSame([Ulid::class, 'fromString'], $fields->get('ulid')->getTypecast()); + $this->assertSame([Ulid::class, 'create'], $fields->get('ulid')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('ulid')->getGenerated()); $this->assertTrue($fields->has('fooUlid')); $this->assertTrue($fields->hasColumn('foo_ulid')); $this->assertSame('ulid', $fields->get('fooUlid')->getType()); - $this->assertSame([Ulid::class, 'fromString'], $fields->get('fooUlid')->getTypecast()); + $this->assertSame([Ulid::class, 'create'], $fields->get('fooUlid')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('fooUlid')->getGenerated()); $this->assertTrue($fields->has('bar')); $this->assertTrue($fields->hasColumn('bar')); $this->assertSame('ulid', $fields->get('bar')->getType()); - $this->assertSame([Ulid::class, 'fromString'], $fields->get('bar')->getTypecast()); + $this->assertSame([Ulid::class, 'create'], $fields->get('bar')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('bar')->getGenerated()); } @@ -96,7 +96,7 @@ public function testAddNullableColumn(ReaderInterface $reader): void $this->assertTrue($fields->has('notDefinedUlid')); $this->assertTrue($fields->hasColumn('not_defined_ulid')); $this->assertSame('ulid', $fields->get('notDefinedUlid')->getType()); - $this->assertSame([Ulid::class, 'fromString'], $fields->get('notDefinedUlid')->getTypecast()); + $this->assertSame([Ulid::class, 'create'], $fields->get('notDefinedUlid')->getTypecast()); $this->assertTrue( $this->registry ->getTableSchema($this->registry->getEntity(NullableUlid::class)) From a5e9f10ec602e342dd3b82c2f2249d6e9c8811fe Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sat, 1 Nov 2025 22:45:12 +1100 Subject: [PATCH 24/51] Updated Ulid following a consistent structure --- src/Ulid.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Ulid.php b/src/Ulid.php index 7a10077..4e6919e 100644 --- a/src/Ulid.php +++ b/src/Ulid.php @@ -87,6 +87,9 @@ public static function create(string $identifier): MaxUlid|NilUlid|UlidIdentifie return (new UlidFactory())->createFromString($identifier); } + /** + * @return array{0: class-string, 1: non-empty-string, 2?: non-empty-array} + */ protected function getTypecast(): array { return [self::class, 'create']; From bbbbc778205ea3d99ab0170d458a19a594bb4266 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:25:05 +1100 Subject: [PATCH 25/51] Adjustments to keep Psalm happy --- src/Snowflake.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Snowflake.php b/src/Snowflake.php index 28fcbfe..f311c09 100644 --- a/src/Snowflake.php +++ b/src/Snowflake.php @@ -23,6 +23,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( @@ -42,6 +43,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->addSnowflakeColumn( From c0e8efd35c38f82c10e5b803330638162a18eb5a Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:26:17 +1100 Subject: [PATCH 26/51] Adjusted code placement --- src/Ulid.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Ulid.php b/src/Ulid.php index 4e6919e..4a1be1a 100644 --- a/src/Ulid.php +++ b/src/Ulid.php @@ -37,6 +37,18 @@ public function __construct( private bool $nullable = false, ) {} + /** + * Create a new Ulid instance from an existing identifier value. + * + * @param non-empty-string $identifier The identifier to create the Ulid from + * + * @see UlidFactory::create() + */ + public static function create(string $identifier): MaxUlid|NilUlid|UlidIdentifier + { + return (new UlidFactory())->createFromString($identifier); + } + #[\Override] public function compute(Registry $registry): void { @@ -75,18 +87,6 @@ public function render(Registry $registry): void ); } - /** - * Create a new Ulid instance from an existing identifier value. - * - * @param non-empty-string $identifier The identifier to create the Ulid from - * - * @see UlidFactory::create() - */ - public static function create(string $identifier): MaxUlid|NilUlid|UlidIdentifier - { - return (new UlidFactory())->createFromString($identifier); - } - /** * @return array{0: class-string, 1: non-empty-string, 2?: non-empty-array} */ From 233fce426ca51ebe01e65ae00e391bfc9dbd21d6 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:27:27 +1100 Subject: [PATCH 27/51] Renamed static factory method and added abstract getTypecast method --- src/Uuid.php | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/Uuid.php b/src/Uuid.php index 9997329..ec19d76 100644 --- a/src/Uuid.php +++ b/src/Uuid.php @@ -8,8 +8,6 @@ use Cycle\ORM\Entity\Behavior\Schema\RegistryModifier; use Cycle\ORM\Schema\GeneratedField; use Cycle\Schema\Registry; -use Ramsey\Identifier\Uuid\UntypedUuid; -use Ramsey\Identifier\Uuid\UuidFactory; abstract class Uuid extends BaseModifier { @@ -17,14 +15,6 @@ abstract class Uuid extends BaseModifier protected string $field; protected bool $nullable = false; - /** - * @param non-empty-string $value - */ - public static function fromString(string $value): UntypedUuid - { - return (new UuidFactory())->createFromString($value); - } - #[\Override] public function compute(Registry $registry): void { @@ -39,7 +29,7 @@ public function compute(Registry $registry): void $modifier->setTypecast( $registry->getEntity($this->role)->getFields()->get($this->field), - [self::class, 'fromString'], + $this->getTypecast(), ); } } @@ -59,7 +49,12 @@ public function render(Registry $registry): void $modifier->setTypecast( $registry->getEntity($this->role)->getFields()->get($this->field), - [self::class, 'fromString'], + $this->getTypecast(), ); } + + /** + * @return array{0: class-string, 1: non-empty-string, 2?: non-empty-array} + */ + abstract protected function getTypecast(): array; } From 8b2d2c4e36bf16c4e69f1ac29006228f1d656d4a Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:28:45 +1100 Subject: [PATCH 28/51] Implemented UUIDv1 factory and getTypecast --- src/Uuid1.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Uuid1.php b/src/Uuid1.php index 0afae6a..611bad8 100644 --- a/src/Uuid1.php +++ b/src/Uuid1.php @@ -9,6 +9,8 @@ use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use JetBrains\PhpStorm\ArrayShape; use Ramsey\Identifier\Service\Nic\Nic; +use Ramsey\Identifier\Uuid\UuidV1; +use Ramsey\Identifier\Uuid\UuidV1Factory; /** * Uses a version 1 (time-based) UUID from a host ID, sequence number, @@ -54,6 +56,22 @@ public function __construct( $this->clockSeq = $clockSeq; } + /** + * Create a new UUIDv1 instance from an existing identifier value. + * + * @param non-empty-string $identifier The identifier to create the Uuid from + */ + public static function create(string $identifier): UuidV1 + { + return (new UuidV1Factory())->createFromString($identifier); + } + + #[\Override] + protected function getTypecast(): array + { + return [self::class, 'create']; + } + #[\Override] protected function getListenerClass(): string { From 0966fafa7216e48e5a140d26de1f8624fb23fa07 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:29:55 +1100 Subject: [PATCH 29/51] Implemented UUIDv2 factory and getTypecast, ensure listener arg localDomain is primitive --- src/Uuid2.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Uuid2.php b/src/Uuid2.php index 424be17..197dceb 100644 --- a/src/Uuid2.php +++ b/src/Uuid2.php @@ -10,6 +10,8 @@ use JetBrains\PhpStorm\ArrayShape; use Ramsey\Identifier\Service\Nic\Nic; use Ramsey\Identifier\Uuid\DceDomain; +use Ramsey\Identifier\Uuid\UuidV2; +use Ramsey\Identifier\Uuid\UuidV2Factory; /** * Uses a version 2 (DCE Security) UUID from a local domain, local @@ -68,6 +70,22 @@ public function __construct( $this->clockSeq = $clockSeq; } + /** + * Create a new UUIDv2 instance from an existing identifier value. + * + * @param non-empty-string $identifier The identifier to create the Uuid from + */ + public static function create(string $identifier): UuidV2 + { + return (new UuidV2Factory())->createFromString($identifier); + } + + #[\Override] + protected function getTypecast(): array + { + return [self::class, 'create']; + } + #[\Override] protected function getListenerClass(): string { @@ -87,7 +105,7 @@ protected function getListenerArgs(): array { return [ 'field' => $this->field, - 'localDomain' => \is_int($this->localDomain) ? DceDomain::from($this->localDomain) : $this->localDomain, + 'localDomain' => $this->localDomain instanceof DceDomain ? $this->localDomain->value : $this->localDomain, 'localIdentifier' => $this->localIdentifier, 'node' => $this->node instanceof Nic ? $this->node->address() : $this->node, 'clockSeq' => $this->clockSeq, From 46d4d3f5240fd206c20c17c9d910c280a9907434 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:31:12 +1100 Subject: [PATCH 30/51] Implemented UUIDv3 factory and getTypecast, made namespace and name optional --- src/Uuid3.php | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/src/Uuid3.php b/src/Uuid3.php index 2e3a5c8..1559365 100644 --- a/src/Uuid3.php +++ b/src/Uuid3.php @@ -10,6 +10,8 @@ use JetBrains\PhpStorm\ArrayShape; use Ramsey\Identifier\Uuid; use Ramsey\Identifier\Uuid\NamespaceId; +use Ramsey\Identifier\Uuid\UuidV3; +use Ramsey\Identifier\Uuid\UuidV3Factory; /** * Uses a version 3 (name-based) UUID based on the MD5 hash of a @@ -23,19 +25,19 @@ final class Uuid3 extends BaseUuid { /** - * @param NamespaceId|Uuid|string $namespace The UUID namespace to use when creating this version 3 identifier - * @param non-empty-string $name The name used to create the version 3 identifier in the given namespace * @param non-empty-string $field Uuid property name * @param non-empty-string|null $column Uuid column name + * @param NamespaceId|Uuid|string|null $namespace The UUID namespace to use when creating this version 3 identifier + * @param non-empty-string|null $name The name used to create the version 3 identifier in the given namespace * @param bool $nullable Indicates whether to generate a new UUID or not * * @see \Ramsey\Identifier\Uuid\UuidFactory::v3() */ public function __construct( - private NamespaceId|Uuid|string $namespace, - private string $name, string $field = 'uuid', ?string $column = null, + private readonly NamespaceId|Uuid|string|null $namespace = null, + private readonly ?string $name = null, bool $nullable = false, ) { $this->field = $field; @@ -43,6 +45,22 @@ public function __construct( $this->nullable = $nullable; } + /** + * Create a new UUIDv3 instance from an existing identifier value. + * + * @param non-empty-string $identifier The identifier to create the Uuid from + */ + public static function create(string $identifier): UuidV3 + { + return (new UuidV3Factory())->createFromString($identifier); + } + + #[\Override] + protected function getTypecast(): array + { + return [self::class, 'create']; + } + #[\Override] protected function getListenerClass(): string { @@ -51,8 +69,8 @@ protected function getListenerClass(): string #[ArrayShape([ 'field' => 'string', - 'namespace' => 'NamespaceId|Uuid|string', - 'name' => 'string', + 'namespace' => 'NamespaceId|Uuid|string|null', + 'name' => 'string|null', 'nullable' => 'bool', ])] #[\Override] @@ -60,7 +78,7 @@ protected function getListenerArgs(): array { return [ 'field' => $this->field, - 'namespace' => $this->namespace, + 'namespace' => $this->namespace instanceof NamespaceId ? $this->namespace->value : $this->namespace, 'name' => $this->name, 'nullable' => $this->nullable, ]; From 8bc4244f99ad723fcfb067423dbad0389df4bc68 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:31:42 +1100 Subject: [PATCH 31/51] Implemented UUIDv4 factory and getTypecast --- src/Uuid4.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Uuid4.php b/src/Uuid4.php index 176aa47..34c8a4b 100644 --- a/src/Uuid4.php +++ b/src/Uuid4.php @@ -8,6 +8,8 @@ use Cycle\ORM\Entity\Behavior\Identifier\Uuid as BaseUuid; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use JetBrains\PhpStorm\ArrayShape; +use Ramsey\Identifier\Uuid\UuidV4; +use Ramsey\Identifier\Uuid\UuidV4Factory; /** * Uses a version 4 (random) UUID @@ -36,6 +38,22 @@ public function __construct( $this->nullable = $nullable; } + /** + * Create a new UUIDv4 instance from an existing identifier value. + * + * @param non-empty-string $identifier The identifier to create the Uuid from + */ + public static function create(string $identifier): UuidV4 + { + return (new UuidV4Factory())->createFromString($identifier); + } + + #[\Override] + protected function getTypecast(): array + { + return [self::class, 'create']; + } + #[\Override] protected function getListenerClass(): string { From 163bed8d508cc1023550a31c48cde3a7bb460a5e Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:31:55 +1100 Subject: [PATCH 32/51] Implemented UUIDv5 factory and getTypecast, made namespace and name optional --- src/Uuid5.php | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Uuid5.php b/src/Uuid5.php index 04fb1b4..6296add 100644 --- a/src/Uuid5.php +++ b/src/Uuid5.php @@ -10,6 +10,8 @@ use JetBrains\PhpStorm\ArrayShape; use Ramsey\Identifier\Uuid; use Ramsey\Identifier\Uuid\NamespaceId; +use Ramsey\Identifier\Uuid\UuidV5; +use Ramsey\Identifier\Uuid\UuidV5Factory; /** * Uses a version 5 (name-based) UUID based on the SHA-1 hash of a @@ -23,19 +25,19 @@ final class Uuid5 extends BaseUuid { /** - * @param NamespaceId|Uuid|string $namespace The UUID namespace to use when creating this version 5 identifier - * @param non-empty-string $name The name used to create the version 5 identifier in the given namespace * @param non-empty-string $field Uuid property name * @param non-empty-string|null $column Uuid column name + * @param NamespaceId|Uuid|string|null $namespace The UUID namespace to use when creating this version 5 identifier + * @param non-empty-string|null $name The name used to create the version 5 identifier in the given namespace * @param bool $nullable Indicates whether to generate a new UUID or not * * @see \Ramsey\Identifier\Uuid\UuidFactory::v5() */ public function __construct( - private NamespaceId|Uuid|string $namespace, - private string $name, string $field = 'uuid', ?string $column = null, + private readonly NamespaceId|Uuid|string|null $namespace = null, + private readonly ?string $name = null, bool $nullable = false, ) { $this->field = $field; @@ -43,6 +45,22 @@ public function __construct( $this->nullable = $nullable; } + /** + * Create a new UUIDv5 instance from an existing identifier value. + * + * @param non-empty-string $identifier The identifier to create the Uuid from + */ + public static function create(string $identifier): UuidV5 + { + return (new UuidV5Factory())->createFromString($identifier); + } + + #[\Override] + protected function getTypecast(): array + { + return [self::class, 'create']; + } + #[\Override] protected function getListenerClass(): string { @@ -51,8 +69,8 @@ protected function getListenerClass(): string #[ArrayShape([ 'field' => 'string', - 'namespace' => 'NamespaceId|Uuid|string', - 'name' => 'string', + 'namespace' => 'NamespaceId|Uuid|string|null', + 'name' => 'string|null', 'nullable' => 'bool', ])] #[\Override] From 605adbf503b18873641bfdf41a9af1501f286a1b Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:32:09 +1100 Subject: [PATCH 33/51] Implemented UUIDv6 factory and getTypecast --- src/Uuid6.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Uuid6.php b/src/Uuid6.php index ce71544..b4a1711 100644 --- a/src/Uuid6.php +++ b/src/Uuid6.php @@ -9,6 +9,8 @@ use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use JetBrains\PhpStorm\ArrayShape; use Ramsey\Identifier\Service\Nic\Nic; +use Ramsey\Identifier\Uuid\UuidV6; +use Ramsey\Identifier\Uuid\UuidV6Factory; /** * Uses a version 6 (ordered-time) UUID from a host ID, sequence number, @@ -54,6 +56,22 @@ public function __construct( $this->clockSeq = $clockSeq; } + /** + * Create a new UUIDv6 instance from an existing identifier value. + * + * @param non-empty-string $identifier The identifier to create the Uuid from + */ + public static function create(string $identifier): UuidV6 + { + return (new UuidV6Factory())->createFromString($identifier); + } + + #[\Override] + protected function getTypecast(): array + { + return [self::class, 'create']; + } + #[\Override] protected function getListenerClass(): string { From 18e62bbea70aad098648624b49bab4ed8d497191 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:32:18 +1100 Subject: [PATCH 34/51] Implemented UUIDv7 factory and getTypecast --- src/Uuid7.php | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Uuid7.php b/src/Uuid7.php index df5f505..3355564 100644 --- a/src/Uuid7.php +++ b/src/Uuid7.php @@ -8,6 +8,8 @@ use Cycle\ORM\Entity\Behavior\Identifier\Uuid as BaseUuid; use Doctrine\Common\Annotations\Annotation\NamedArgumentConstructor; use JetBrains\PhpStorm\ArrayShape; +use Ramsey\Identifier\Uuid\UuidV7; +use Ramsey\Identifier\Uuid\UuidV7Factory; /** * Uses a version 7 (Unix Epoch Time) UUID @@ -36,6 +38,22 @@ public function __construct( $this->nullable = $nullable; } + /** + * Create a new UUIDv7 instance from an existing identifier value. + * + * @param non-empty-string $identifier The identifier to create the Uuid from + */ + public static function create(string $identifier): UuidV7 + { + return (new UuidV7Factory())->createFromString($identifier); + } + + #[\Override] + protected function getTypecast(): array + { + return [self::class, 'create']; + } + #[\Override] protected function getListenerClass(): string { From 7fd75d3415e051e9e5696656452ec0b8a5203b4f Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:33:17 +1100 Subject: [PATCH 35/51] Ensure SnowflakeDiscord getProcessId returns int within range --- src/Listener/SnowflakeDiscord.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Listener/SnowflakeDiscord.php b/src/Listener/SnowflakeDiscord.php index 16dee72..87fd9e0 100644 --- a/src/Listener/SnowflakeDiscord.php +++ b/src/Listener/SnowflakeDiscord.php @@ -71,6 +71,7 @@ protected function createValue(): DiscordSnowflake */ private function getProcessId(): int { - return self::$defaultProcessId ?? \intval(\getmypid()); + $processId = self::$defaultProcessId ?? \intval(\getmypid()); + return \min(\max($processId, 0), 281474976710655); } } From 2aac843478640dcc82ff450964c146a9138f3ad4 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:34:39 +1100 Subject: [PATCH 36/51] Added UUID factory to base UUID Listener --- src/Listener/BaseUuid.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Listener/BaseUuid.php b/src/Listener/BaseUuid.php index 046d31a..c7aaa76 100644 --- a/src/Listener/BaseUuid.php +++ b/src/Listener/BaseUuid.php @@ -6,9 +6,13 @@ use Cycle\ORM\Entity\Behavior\Attribute\Listen; use Cycle\ORM\Entity\Behavior\Event\Mapper\Command\OnCreate; +use Ramsey\Identifier\Uuid; +use Ramsey\Identifier\Uuid\UuidFactory; abstract class BaseUuid { + protected UuidFactory $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 @@ -16,7 +20,9 @@ abstract class BaseUuid public function __construct( private readonly string $field, private readonly bool $nullable = false, - ) {} + ) { + $this->factory = new UuidFactory(); + } #[Listen(OnCreate::class)] public function __invoke(OnCreate $event): void @@ -28,5 +34,5 @@ public function __invoke(OnCreate $event): void $event->state->register($this->field, $this->createValue()); } - abstract protected function createValue(): \Ramsey\Identifier\Uuid; + abstract protected function createValue(): Uuid; } From 2d61e62133f7f52e47ec43ee3ff1bd3e10d409b5 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:36:53 +1100 Subject: [PATCH 37/51] Updated UUIDv1 Listener to use factory, changed return type to more specific value --- src/Listener/Uuid1.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Listener/Uuid1.php b/src/Listener/Uuid1.php index 5bc7dea..f7238a9 100644 --- a/src/Listener/Uuid1.php +++ b/src/Listener/Uuid1.php @@ -4,7 +4,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; -use Ramsey\Identifier\Uuid\UuidV1Factory; +use Ramsey\Identifier\Uuid\UuidV1; /** * Generates UUIDv1 identifiers for entities. @@ -16,7 +16,6 @@ final class Uuid1 extends BaseUuid private static int|string|null $defaultNode = null; private static ?int $defaultClockSeq = null; - private UuidV1Factory $factory; /** * @param non-empty-string $field The name of the field to store the UUID @@ -30,7 +29,6 @@ public function __construct( private readonly int|string|null $node = null, private readonly ?int $clockSeq = null, ) { - $this->factory = new UuidV1Factory(); parent::__construct($field, $nullable); } @@ -47,10 +45,10 @@ public static function setDefaults(int|string|null $node, ?int $clockSeq): void } #[\Override] - protected function createValue(): \Ramsey\Identifier\Uuid + protected function createValue(): UuidV1 { $node = $this->node ?? self::$defaultNode; $clockSeq = $this->clockSeq ?? self::$defaultClockSeq; - return $this->factory->create($node, $clockSeq); + return $this->factory->v1($node, $clockSeq); } } From af7cd675c2a035055623a3ae640ad0c8b67a9a5e Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:37:12 +1100 Subject: [PATCH 38/51] Updated UUIDv2 Listener to use factory, changed return type to more specific value --- src/Listener/Uuid2.php | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Listener/Uuid2.php b/src/Listener/Uuid2.php index 098c023..ba00a3b 100644 --- a/src/Listener/Uuid2.php +++ b/src/Listener/Uuid2.php @@ -5,7 +5,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; use Ramsey\Identifier\Uuid\DceDomain; -use Ramsey\Identifier\Uuid\UuidV2Factory; +use Ramsey\Identifier\Uuid\UuidV2; /** * Generates UUIDv2 (DCE Security) identifiers for entities. @@ -22,8 +22,6 @@ final class Uuid2 extends BaseUuid private static int|string|null $defaultNode = null; private static ?int $defaultClockSeq = null; - private readonly UuidV2Factory $factory; - private readonly ?DceDomain $localDomain; /** * @param non-empty-string $field The name of the field to store the UUID @@ -36,13 +34,11 @@ final class Uuid2 extends BaseUuid public function __construct( string $field, bool $nullable = false, - DceDomain|int|null $localDomain = null, + 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->localDomain = \is_int($localDomain) ? DceDomain::from($localDomain) : $localDomain; - $this->factory = new UuidV2Factory(); parent::__construct($field, $nullable); } @@ -60,16 +56,14 @@ public static function setDefaults( int|string|null $node, ?int $clockSeq, ): void { - if ($localDomain !== null) { - self::$defaultLocalDomain = $localDomain; - } + self::$defaultLocalDomain = $localDomain; self::$defaultLocalIdentifier = $localIdentifier; self::$defaultNode = $node; self::$defaultClockSeq = $clockSeq; } #[\Override] - protected function createValue(): \Ramsey\Identifier\Uuid + protected function createValue(): UuidV2 { $localDomain = $this->localDomain ?? self::$defaultLocalDomain; $localIdentifier = $this->localIdentifier ?? self::$defaultLocalIdentifier; @@ -78,6 +72,6 @@ protected function createValue(): \Ramsey\Identifier\Uuid $localDomain = \is_int($localDomain) ? DceDomain::from($localDomain) : $localDomain; - return $this->factory->create($localDomain, $localIdentifier, $node, $clockSeq); + return $this->factory->v2($localDomain, $localIdentifier, $node, $clockSeq); } } From e430af8706d55c521e8988daaaf152226ff96f5d Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:37:43 +1100 Subject: [PATCH 39/51] Updated UUIDv2 Listener to use factory, changed return type to more specific value, added support for default values --- src/Listener/Uuid3.php | 43 +++++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/src/Listener/Uuid3.php b/src/Listener/Uuid3.php index 493d490..906639f 100644 --- a/src/Listener/Uuid3.php +++ b/src/Listener/Uuid3.php @@ -7,34 +7,59 @@ 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\UuidV3; /** * Generates UUIDv3 (name-based with MD5 hashing) identifiers for entities. */ final class Uuid3 extends Base { - private readonly UuidFactory $factory; + private static NamespaceId|Uuid|string|null $defaultNamespace = null; + private static ?string $defaultName = null; /** * @param non-empty-string $field The name of the field to store the 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 + * @param NamespaceId|Uuid|string|null $namespace The namespace UUID + * @param string|null $name The name to hash */ public function __construct( string $field, - private readonly NamespaceId|Uuid|string $namespace, - private readonly string $name, bool $nullable = false, + private readonly NamespaceId|Uuid|string|null $namespace = null, + private readonly ?string $name = null, ) { - $this->factory = new UuidFactory(); parent::__construct($field, $nullable); } + /** + * Set default values for UUIDv3 generation. + * + * @param NamespaceId|Uuid|string|null $namespace + * @param string|null $name + */ + public static function setDefaults( + NamespaceId|Uuid|string|null $namespace, + ?string $name, + ): void { + self::$defaultNamespace = $namespace; + self::$defaultName = $name; + } + #[\Override] - protected function createValue(): \Ramsey\Identifier\Uuid + protected function createValue(): UuidV3 { - return $this->factory->v3($this->namespace, $this->name); + $namespace = $this->namespace ?? self::$defaultNamespace; + $name = $this->name ?? self::$defaultName; + + if ($namespace === null) { + throw new \InvalidArgumentException('Namespace must be specified.'); + } + + if ($name === null) { + throw new \InvalidArgumentException('Name must be specified.'); + } + + return $this->factory->v3($namespace, $name); } } From 47d01a50187a690764adaebf7ad9dd4df763104b Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:37:54 +1100 Subject: [PATCH 40/51] Updated UUIDv2 Listener to use factory, changed return type to more specific value --- src/Listener/Uuid4.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Listener/Uuid4.php b/src/Listener/Uuid4.php index fff09eb..b1e41c2 100644 --- a/src/Listener/Uuid4.php +++ b/src/Listener/Uuid4.php @@ -4,15 +4,13 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; -use Ramsey\Identifier\Uuid\UuidV4Factory; +use Ramsey\Identifier\Uuid\UuidV4; /** * 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 @@ -21,13 +19,12 @@ public function __construct( string $field, bool $nullable = false, ) { - $this->factory = new UuidV4Factory(); parent::__construct($field, $nullable); } #[\Override] - protected function createValue(): \Ramsey\Identifier\Uuid + protected function createValue(): UuidV4 { - return $this->factory->create(); + return $this->factory->v4(); } } From ef86bddfae216f07b6fe2a2f3bff110325e5ac4a Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:38:16 +1100 Subject: [PATCH 41/51] Updated UUIDv5 Listener to use factory, changed return type to more specific value, added support for default values --- src/Listener/Uuid5.php | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/Listener/Uuid5.php b/src/Listener/Uuid5.php index 6de1cac..e070696 100644 --- a/src/Listener/Uuid5.php +++ b/src/Listener/Uuid5.php @@ -7,34 +7,57 @@ 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\UuidV5; /** * Generates UUIDv5 (name-based with SHA-1 hashing) identifiers for entities. */ final class Uuid5 extends Base { - private readonly UuidFactory $factory; + private static NamespaceId|Uuid|string|null $defaultNamespace = null; + private static ?string $defaultName = null; /** * @param non-empty-string $field The name of the field to store the 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 + * @param NamespaceId|Uuid|string|null $namespace The namespace UUID + * @param string|null $name The name to hash */ public function __construct( string $field, - private readonly NamespaceId|Uuid|string $namespace, - private readonly string $name, bool $nullable = false, + private readonly NamespaceId|Uuid|string|null $namespace = null, + private readonly ?string $name = null, ) { - $this->factory = new UuidFactory(); parent::__construct($field, $nullable); } + /** + * Set default values for UUIDv3 generation. + * + */ + public static function setDefaults( + NamespaceId|Uuid|string|null $namespace, + ?string $name, + ): void { + self::$defaultNamespace = $namespace; + self::$defaultName = $name; + } + #[\Override] - protected function createValue(): \Ramsey\Identifier\Uuid + protected function createValue(): UuidV5 { - return $this->factory->v5($this->namespace, $this->name); + $namespace = $this->namespace ?? self::$defaultNamespace; + $name = $this->name ?? self::$defaultName; + + if ($namespace === null) { + throw new \InvalidArgumentException('Namespace must be specified.'); + } + + if ($name === null) { + throw new \InvalidArgumentException('Name must be specified.'); + } + + return $this->factory->v5($namespace, $name); } } From 6240cd899eb119f61538f5f86dc2a579b9b417fe Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:38:26 +1100 Subject: [PATCH 42/51] Updated UUIDv6 Listener to use factory, changed return type to more specific value --- src/Listener/Uuid6.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Listener/Uuid6.php b/src/Listener/Uuid6.php index a39a2cf..cb24e6c 100644 --- a/src/Listener/Uuid6.php +++ b/src/Listener/Uuid6.php @@ -5,7 +5,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; use Ramsey\Identifier\Service\Nic\Nic; -use Ramsey\Identifier\Uuid\UuidV6Factory; +use Ramsey\Identifier\Uuid\UuidV6; /** * Generates UUIDv6 (ordered-time) identifiers for entities. @@ -17,7 +17,6 @@ final class Uuid6 extends BaseUuid private static int|string|null $defaultNode = null; private static ?int $defaultClockSeq = null; - private UuidV6Factory $factory; /** * @param non-empty-string $field The name of the field to store the UUID @@ -31,7 +30,6 @@ public function __construct( private readonly int|string|null $node = null, private readonly ?int $clockSeq = null, ) { - $this->factory = new UuidV6Factory(); parent::__construct($field, $nullable); } @@ -48,11 +46,11 @@ public static function setDefaults(int|string|null $node, ?int $clockSeq): void } #[\Override] - protected function createValue(): \Ramsey\Identifier\Uuid + protected function createValue(): UuidV6 { $node = $this->node ?? self::$defaultNode; $clockSeq = $this->clockSeq ?? self::$defaultClockSeq; $node = $node instanceof Nic ? $node->address() : $node; - return $this->factory->create($node, $clockSeq); + return $this->factory->v6($node, $clockSeq); } } From 3c87203dd8ad42e1d03fe463911c745c29ecb7e2 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:38:33 +1100 Subject: [PATCH 43/51] Updated UUIDv7 Listener to use factory, changed return type to more specific value --- src/Listener/Uuid7.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Listener/Uuid7.php b/src/Listener/Uuid7.php index 522e5df..3a82d21 100644 --- a/src/Listener/Uuid7.php +++ b/src/Listener/Uuid7.php @@ -4,15 +4,13 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Listener; -use Ramsey\Identifier\Uuid\UuidV7Factory; +use Ramsey\Identifier\Uuid\UuidV7; /** * 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 @@ -21,13 +19,12 @@ public function __construct( string $field, bool $nullable = false, ) { - $this->factory = new UuidV7Factory(); parent::__construct($field, $nullable); } #[\Override] - protected function createValue(): \Ramsey\Identifier\Uuid + protected function createValue(): UuidV7 { - return $this->factory->create(); + return $this->factory->v7(); } } From 21bc7b7fd0cbfb24b6aef39ae113f14119601fe2 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:39:00 +1100 Subject: [PATCH 44/51] Additional test fixtures --- .../Fixtures/Snowflake/AllSnowflake.php | 63 +++++++++++++++ tests/Identifier/Fixtures/Ulid/AllUlid.php | 31 ++++++++ tests/Identifier/Fixtures/Uuid/AllUuid.php | 79 +++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 tests/Identifier/Fixtures/Snowflake/AllSnowflake.php create mode 100644 tests/Identifier/Fixtures/Ulid/AllUlid.php create mode 100644 tests/Identifier/Fixtures/Uuid/AllUuid.php diff --git a/tests/Identifier/Fixtures/Snowflake/AllSnowflake.php b/tests/Identifier/Fixtures/Snowflake/AllSnowflake.php new file mode 100644 index 0000000..cb56860 --- /dev/null +++ b/tests/Identifier/Fixtures/Snowflake/AllSnowflake.php @@ -0,0 +1,63 @@ + Date: Sun, 2 Nov 2025 18:39:18 +1100 Subject: [PATCH 45/51] Updated test cases --- .../Driver/Common/Combined/CombinedTest.php | 8 +- .../Driver/Common/Combined/ListenerTest.php | 8 +- .../Driver/Common/Snowflake/ListenerTest.php | 363 +++++++++++------ .../Driver/Common/Ulid/ListenerTest.php | 81 ++-- .../Driver/Common/Uuid/ListenerTest.php | 364 ++++++++++-------- .../Driver/Common/Uuid/UuidTest.php | 18 +- tests/Identifier/Unit/Uuid2Test.php | 20 +- tests/Identifier/Unit/Uuid3Test.php | 58 ++- tests/Identifier/Unit/Uuid5Test.php | 57 ++- 9 files changed, 630 insertions(+), 347 deletions(-) diff --git a/tests/Identifier/Functional/Driver/Common/Combined/CombinedTest.php b/tests/Identifier/Functional/Driver/Common/Combined/CombinedTest.php index 8198366..7588218 100644 --- a/tests/Identifier/Functional/Driver/Common/Combined/CombinedTest.php +++ b/tests/Identifier/Functional/Driver/Common/Combined/CombinedTest.php @@ -35,25 +35,25 @@ public function testColumnsExist(ReaderInterface $reader): void $this->assertTrue($fields->has('uuid')); $this->assertTrue($fields->hasColumn('uuid')); $this->assertSame('uuid', $fields->get('uuid')->getType()); - $this->assertSame([Uuid::class, 'fromString'], $fields->get('uuid')->getTypecast()); + $this->assertSame([Uuid::class, 'create'], $fields->get('uuid')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('uuid')->getGenerated()); $this->assertTrue($fields->has('uuidNullable')); $this->assertTrue($fields->hasColumn('uuid_nullable')); $this->assertSame('uuid', $fields->get('uuidNullable')->getType()); - $this->assertSame([Uuid::class, 'fromString'], $fields->get('uuidNullable')->getTypecast()); + $this->assertSame([Uuid::class, 'create'], $fields->get('uuidNullable')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('uuidNullable')->getGenerated()); $this->assertTrue($fields->has('ulid')); $this->assertTrue($fields->hasColumn('ulid')); $this->assertSame('ulid', $fields->get('ulid')->getType()); - $this->assertSame([Ulid::class, 'fromString'], $fields->get('ulid')->getTypecast()); + $this->assertSame([Ulid::class, 'create'], $fields->get('ulid')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('ulid')->getGenerated()); $this->assertTrue($fields->has('ulidNullable')); $this->assertTrue($fields->hasColumn('ulid_nullable')); $this->assertSame('ulid', $fields->get('ulidNullable')->getType()); - $this->assertSame([Ulid::class, 'fromString'], $fields->get('ulidNullable')->getTypecast()); + $this->assertSame([Ulid::class, 'create'], $fields->get('ulidNullable')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('ulidNullable')->getGenerated()); } diff --git a/tests/Identifier/Functional/Driver/Common/Combined/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Combined/ListenerTest.php index 203ecc2..74e9bfe 100644 --- a/tests/Identifier/Functional/Driver/Common/Combined/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Combined/ListenerTest.php @@ -153,10 +153,10 @@ public function withListeners(array|string $listeners): void SchemaInterface::SCHEMA => [], SchemaInterface::RELATIONS => [], SchemaInterface::TYPECAST => [ - 'uuid' => [Uuid::class, 'fromString'], - 'uuid_nullable' => [Uuid::class, 'fromString'], - 'ulid' => [Ulid::class, 'fromString'], - 'ulid_nullable' => [Ulid::class, 'fromString'], + 'uuid' => [Uuid::class, 'create'], + 'uuid_nullable' => [Uuid::class, 'create'], + 'ulid' => [Ulid::class, 'create'], + 'ulid_nullable' => [Ulid::class, 'create'], 'snowflake' => [$factory, 'createFromInteger'], 'snowflake_nullable' => [$factory, 'createFromInteger'], ], diff --git a/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php index ebbb5e6..6e1b420 100644 --- a/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php @@ -4,7 +4,12 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Tests\Functional\Driver\Common\Snowflake; -use Cycle\ORM\Entity\Behavior\Identifier\Tests\Fixtures\Snowflake\User; +use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeDiscord; +use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeGeneric; +use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeInstagram; +use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeMastodon; +use Cycle\ORM\Entity\Behavior\Identifier\SnowflakeTwitter; +use Cycle\ORM\Entity\Behavior\Identifier\Tests\Fixtures\Snowflake\AllSnowflake; 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\SnowflakeDiscord as SnowflakeDiscordListener; @@ -16,122 +21,215 @@ use Cycle\ORM\Schema; use Cycle\ORM\SchemaInterface; use Cycle\ORM\Select; +use Ramsey\Identifier\Snowflake\DiscordSnowflake; use Ramsey\Identifier\Snowflake\DiscordSnowflakeFactory; +use Ramsey\Identifier\Snowflake\GenericSnowflake; use Ramsey\Identifier\Snowflake\GenericSnowflakeFactory; +use Ramsey\Identifier\Snowflake\InstagramSnowflake; use Ramsey\Identifier\Snowflake\InstagramSnowflakeFactory; +use Ramsey\Identifier\Snowflake\MastodonSnowflake; use Ramsey\Identifier\Snowflake\MastodonSnowflakeFactory; +use Ramsey\Identifier\Snowflake\TwitterSnowflake; use Ramsey\Identifier\Snowflake\TwitterSnowflakeFactory; -use Ramsey\Identifier\Snowflake as SnowflakeInterface; abstract class ListenerTest extends BaseTest { use TableTrait; - public function testAssignManually(): void + public function testNullable(): void { $this->withListeners([ SnowflakeGenericListener::class, [ - 'field' => 'snowflake', + 'field' => 'generic', + 'nullable' => true, + ], + SnowflakeGenericListener::class, + [ + 'field' => 'discord', + 'nullable' => true, + ], + SnowflakeGenericListener::class, + [ + 'field' => 'instagram', + 'nullable' => true, + ], + SnowflakeGenericListener::class, + [ + 'field' => 'mastodon', + 'nullable' => true, + ], + SnowflakeGenericListener::class, + [ + 'field' => 'twitter', + 'nullable' => true, ], ]); - $user = new User(); - $user->snowflake = (new GenericSnowflakeFactory(0, 0))->create(); - $bytes = $user->snowflake->toBytes(); + $entity = new AllSnowflake(); + $this->save($entity); + + $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); + $data = $select->fetchOne(); + + $this->assertNull($data->generic); + $this->assertNull($data->discord); + $this->assertNull($data->instagram); + $this->assertNull($data->mastodon); + $this->assertNull($data->twitter); + } + + public function testAssignManually(): void + { + $this->withListeners(); + + $entity = new AllSnowflake(); + $entity->generic = (new GenericSnowflakeFactory(10, 1662744255000))->create(); + $entity->discord = (new DiscordSnowflakeFactory(10, 20))->create(); + $entity->instagram = (new InstagramSnowflakeFactory(10))->create(); + $entity->mastodon = (new MastodonSnowflakeFactory('users'))->create(); + $entity->twitter = (new TwitterSnowflakeFactory(10))->create(); - $this->save($user); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); $data = $select->fetchOne(); - $this->assertSame($bytes, $data->snowflake->toBytes()); + $this->assertSame($entity->generic->toString(), $data->generic->toString()); + $this->assertSame($entity->discord->toString(), $data->discord->toString()); + $this->assertSame($entity->instagram->toString(), $data->instagram->toString()); + $this->assertSame($entity->mastodon->toString(), $data->mastodon->toString()); + $this->assertSame($entity->twitter->toString(), $data->twitter->toString()); } - public function testDiscordSnowflake(): void + public function testGenericSnowflake(): void { $this->withListeners([ - SnowflakeDiscordListener::class, + SnowflakeGenericListener::class, [ - 'field' => 'snowflake', - 'workerId' => 10, - 'processId' => 20, + 'field' => 'generic', + 'node' => 10, + 'epochOffset' => 1662744255000, ], ]); - $user = new User(); - $this->save($user); + $entity = new AllSnowflake(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); $data = $select->fetchOne(); - $this->assertInstanceOf(SnowflakeInterface::class, $data->snowflake); - $this->assertIsString($data->snowflake->toBytes()); - $this->assertIsString($data->snowflake->toString()); + $this->assertInstanceOf(GenericSnowflake::class, $data->generic); + $this->assertIsString($data->generic->toString()); + $this->assertSame(18, \strlen($data->generic->toString())); } - public function testNullableDiscordSnowflake(): void + public function testGenericDefaults(): void { + SnowflakeGenericListener::setDefaults(20, 1662744250000); + $this->withListeners([ - SnowflakeDiscordListener::class, + SnowflakeGenericListener::class, [ - 'field' => 'foo_snowflake', - 'nullable' => true, + 'field' => 'generic', ], ]); - $user = new User(); - $user->snowflake = (new DiscordSnowflakeFactory(0, 0))->create(); + $entity = new AllSnowflake(); + $this->save($entity); + + $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); + $data = $select->fetchOne(); - $this->save($user); + $this->assertInstanceOf(GenericSnowflake::class, $data->generic); + $this->assertIsString($data->generic->toString()); + $this->assertSame(18, \strlen($data->generic->toString())); + } - $select = new Select($this->orm->with(heap: new Heap()), User::class); - $data = $select->fetchData(); + public function testGenericNodeMinimumThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); - $this->assertNull($data[0]['foo_snowflake']); + SnowflakeGenericListener::setDefaults(-1, 1662744250000); } - public function testGenericSnowflake(): void + public function testGenericNodeMaximumThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); + + SnowflakeGenericListener::setDefaults(1024, 1662744250000); + } + + public function testDiscordSnowflake(): void { $this->withListeners([ - SnowflakeGenericListener::class, + SnowflakeDiscordListener::class, [ - 'field' => 'snowflake', - 'node' => 10, - 'epochOffset' => 1662744255000, + 'field' => 'discord', + 'workerId' => 10, + 'processId' => 20, ], ]); - $user = new User(); - $this->save($user); + $entity = new AllSnowflake(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); $data = $select->fetchOne(); - $this->assertInstanceOf(SnowflakeInterface::class, $data->snowflake); - $this->assertIsString($data->snowflake->toBytes()); - $this->assertIsString($data->snowflake->toString()); + $this->assertInstanceOf(DiscordSnowflake::class, $data->discord); + $this->assertIsString($data->discord->toString()); + $this->assertSame(19, \strlen($data->discord->toString())); } - public function testNullableGenericSnowflake(): void + public function testDiscordDefaults(): void { + SnowflakeDiscordListener::setDefaults(20, 30); + $this->withListeners([ - SnowflakeGenericListener::class, + SnowflakeDiscordListener::class, [ - 'field' => 'foo_snowflake', - 'nullable' => true, + 'field' => 'discord', ], ]); - $user = new User(); - $user->snowflake = (new GenericSnowflakeFactory(0, 0))->create(); + $entity = new AllSnowflake(); + $this->save($entity); + + $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); + $data = $select->fetchOne(); + + $this->assertInstanceOf(DiscordSnowflake::class, $data->discord); + $this->assertIsString($data->discord->toString()); + $this->assertSame(19, \strlen($data->discord->toString())); + } + + public function testDiscordWorkerIdMinimumThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); + + SnowflakeDiscordListener::setDefaults(-1, 30); + } - $this->save($user); + public function testDiscordWorkerIdMaximumThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); - $select = new Select($this->orm->with(heap: new Heap()), User::class); - $data = $select->fetchData(); + SnowflakeDiscordListener::setDefaults(281474976710656, 30); + } - $this->assertNull($data[0]['foo_snowflake']); + public function testDiscordProcessIdMinimumThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); + + SnowflakeDiscordListener::setDefaults(20, -1); + } + + public function testDiscordProcessIdMaximumThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); + + SnowflakeDiscordListener::setDefaults(20, 281474976710656); } public function testInstagramSnowflake(): void @@ -139,83 +237,77 @@ public function testInstagramSnowflake(): void $this->withListeners([ SnowflakeInstagramListener::class, [ - 'field' => 'snowflake', + 'field' => 'instagram', 'shardId' => 10, ], ]); - $user = new User(); - $this->save($user); + $entity = new AllSnowflake(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); $data = $select->fetchOne(); - $this->assertInstanceOf(SnowflakeInterface::class, $data->snowflake); - $this->assertIsString($data->snowflake->toBytes()); - $this->assertIsString($data->snowflake->toString()); + $this->assertInstanceOf(InstagramSnowflake::class, $data->instagram); + $this->assertIsString($data->instagram->toString()); + $this->assertSame(19, \strlen($data->instagram->toString())); } - public function testNullableInstagramSnowflake(): void + public function testInstagramDefaults(): void { + SnowflakeInstagramListener::setDefaults(20); + $this->withListeners([ SnowflakeInstagramListener::class, [ - 'field' => 'foo_snowflake', - 'nullable' => true, + 'field' => 'instagram', ], ]); - $user = new User(); - $user->snowflake = (new InstagramSnowflakeFactory(0))->create(); - - $this->save($user); + $entity = new AllSnowflake(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); - $data = $select->fetchData(); + $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); + $data = $select->fetchOne(); - $this->assertNull($data[0]['foo_snowflake']); + $this->assertInstanceOf(InstagramSnowflake::class, $data->instagram); + $this->assertIsString($data->instagram->toString()); + $this->assertSame(19, \strlen($data->instagram->toString())); } - public function testMastodonSnowflake(): void + public function testInstagramShardIdMinimumThrowsException(): void { - $this->withListeners([ - SnowflakeMastodonListener::class, - [ - 'field' => 'snowflake', - 'tableName' => 'users', - ], - ]); + $this->expectException(\InvalidArgumentException::class); - $user = new User(); - $this->save($user); + SnowflakeInstagramListener::setDefaults(-1); + } - $select = new Select($this->orm->with(heap: new Heap()), User::class); - $data = $select->fetchOne(); + public function testInstagramShardIdMaximumThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); - $this->assertInstanceOf(SnowflakeInterface::class, $data->snowflake); - $this->assertIsString($data->snowflake->toBytes()); - $this->assertIsString($data->snowflake->toString()); + SnowflakeInstagramListener::setDefaults(1024); } - public function testNullableMastodonSnowflake(): void + public function testMastodonSnowflake(): void { $this->withListeners([ SnowflakeMastodonListener::class, [ - 'field' => 'foo_snowflake', - 'nullable' => true, + 'field' => 'mastodon', + 'tableName' => 'users', ], ]); - $user = new User(); - $user->snowflake = (new MastodonSnowflakeFactory(null))->create(); + $entity = new AllSnowflake(); + $this->save($entity); - $this->save($user); - - $select = new Select($this->orm->with(heap: new Heap()), User::class); - $data = $select->fetchData(); + $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); + $data = $select->fetchOne(); - $this->assertNull($data[0]['foo_snowflake']); + $this->assertInstanceOf(MastodonSnowflake::class, $data->mastodon); + $this->assertIsString($data->mastodon->toString()); + $this->assertSame(18, \strlen($data->mastodon->toString())); } public function testTwitterSnowflake(): void @@ -223,60 +315,76 @@ public function testTwitterSnowflake(): void $this->withListeners([ SnowflakeTwitterListener::class, [ - 'field' => 'snowflake', + 'field' => 'twitter', 'machineId' => 10, ], ]); - $user = new User(); - $this->save($user); + $entity = new AllSnowflake(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); $data = $select->fetchOne(); - $this->assertInstanceOf(SnowflakeInterface::class, $data->snowflake); - $this->assertIsString($data->snowflake->toBytes()); - $this->assertIsString($data->snowflake->toString()); + $this->assertInstanceOf(TwitterSnowflake::class, $data->twitter); + $this->assertIsString($data->twitter->toString()); + $this->assertSame(19, \strlen($data->twitter->toString())); } - public function testNullableTwitterSnowflake(): void + public function testTwitterDefaults(): void { + SnowflakeTwitterListener::setDefaults(20); + $this->withListeners([ SnowflakeTwitterListener::class, [ - 'field' => 'foo_snowflake', - 'nullable' => true, + 'field' => 'twitter', ], ]); - $user = new User(); - $user->snowflake = (new TwitterSnowflakeFactory(0))->create(); + $entity = new AllSnowflake(); + $this->save($entity); + + $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); + $data = $select->fetchOne(); - $this->save($user); + $this->assertInstanceOf(TwitterSnowflake::class, $data->twitter); + $this->assertIsString($data->twitter->toString()); + $this->assertSame(19, \strlen($data->twitter->toString())); + } - $select = new Select($this->orm->with(heap: new Heap()), User::class); - $data = $select->fetchData(); + public function testTwitterMachineIdMinimumThrowsException(): void + { + $this->expectException(\InvalidArgumentException::class); - $this->assertNull($data[0]['foo_snowflake']); + SnowflakeTwitterListener::setDefaults(-1); } - public function withListeners(array|string $listeners): void + public function testTwitterMachineIdMaximumThrowsException(): void { - $factory = new GenericSnowflakeFactory(0, 0); + $this->expectException(\InvalidArgumentException::class); + + SnowflakeTwitterListener::setDefaults(1024); + } + public function withListeners(array|string|null $listeners = null): void + { $this->withSchema(new Schema([ - User::class => [ - SchemaInterface::ROLE => 'user', + AllSnowflake::class => [ + SchemaInterface::ROLE => 'all_snowflake', SchemaInterface::DATABASE => 'default', - SchemaInterface::TABLE => 'users', - SchemaInterface::PRIMARY_KEY => 'snowflake', - SchemaInterface::COLUMNS => ['snowflake', 'foo_snowflake'], - SchemaInterface::LISTENERS => [$listeners], + SchemaInterface::TABLE => 'all_snowflakes', + SchemaInterface::PRIMARY_KEY => 'id', + SchemaInterface::COLUMNS => ['id', 'generic', 'discord', 'instagram', 'mastodon', 'twitter'], + SchemaInterface::LISTENERS => $listeners ? [$listeners] : [], SchemaInterface::SCHEMA => [], SchemaInterface::RELATIONS => [], SchemaInterface::TYPECAST => [ - 'snowflake' => [$factory, 'createFromInteger'], - 'foo_snowflake' => [$factory, 'createFromInteger'], + 'generic' => [SnowflakeGeneric::class, 'create', [10]], + 'discord' => [SnowflakeDiscord::class, 'create'], + 'instagram' => [SnowflakeInstagram::class, 'create'], + 'mastodon' => [SnowflakeMastodon::class, 'create'], + 'twitter' => [SnowflakeTwitter::class, 'create'], ], ], ])); @@ -287,11 +395,20 @@ public function setUp(): void { parent::setUp(); + SnowflakeGenericListener::setDefaults(0, 0); + SnowflakeDiscordListener::setDefaults(0, null); + SnowflakeInstagramListener::setDefaults(0); + SnowflakeTwitterListener::setDefaults(0); + $this->makeTable( - 'users', + 'all_snowflakes', [ - 'snowflake' => 'snowflake', - 'foo_snowflake' => 'snowflake,nullable', + 'id' => 'integer', + 'generic' => 'snowflake,nullable', + 'discord' => 'snowflake,nullable', + 'instagram' => 'snowflake,nullable', + 'mastodon' => 'snowflake,nullable', + 'twitter' => 'snowflake,nullable', ], ); } diff --git a/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php index 6764f8f..0ef4ed0 100644 --- a/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Ulid/ListenerTest.php @@ -4,7 +4,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Tests\Functional\Driver\Common\Ulid; -use Cycle\ORM\Entity\Behavior\Identifier\Tests\Fixtures\Ulid\User; +use Cycle\ORM\Entity\Behavior\Identifier\Tests\Fixtures\Ulid\AllUlid; 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\Ulid as UlidListener; @@ -20,73 +20,74 @@ abstract class ListenerTest extends BaseTest { use TableTrait; - public function testAssignManually(): void + public function testNullable(): void { - $this->withListeners(UlidListener::class); - - $user = new User(); - $user->ulid = (new UlidFactory())->create(); - $bytes = $user->ulid->toBytes(); + $this->withListeners([ + UlidListener::class, + [ + 'field' => 'ulid', + 'nullable' => true, + ], + ]); - $this->save($user); + $entity = new AllUlid(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllUlid::class); $data = $select->fetchOne(); - $this->assertSame($bytes, $data->ulid->toBytes()); + $this->assertNull($data->ulid); } - public function testWithNullableTrue(): void + public function testAssignManually(): void { - $this->withListeners([ - UlidListener::class, - [ - 'field' => 'foo_ulid', - 'nullable' => true, - ], - ]); + $this->withListeners(); - $user = new User(); - $user->ulid = (new UlidFactory())->create(); + $entity = new AllUlid(); + $entity->ulid = (new UlidFactory())->create(); - $this->save($user); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); - $data = $select->fetchData(); + $select = new Select($this->orm->with(heap: new Heap()), AllUlid::class); + $data = $select->fetchOne(); - $this->assertNull($data[0]['foo_ulid']); + $this->assertSame($entity->ulid->toString(), $data->ulid->toString()); } public function testUlid(): void { - $this->withListeners(UlidListener::class); + $this->withListeners([ + UlidListener::class, + [ + 'field' => 'ulid', + ], + ]); - $user = new User(); - $this->save($user); + $entity = new AllUlid(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllUlid::class); $data = $select->fetchOne(); $this->assertInstanceOf(UlidInterface::class, $data->ulid); - $this->assertIsString($data->ulid->toBytes()); $this->assertIsString($data->ulid->toString()); + $this->assertSame(26, \strlen($data->ulid->toString())); } - public function withListeners(array|string $listeners): void + public function withListeners(array|string|null $listeners = null): void { $this->withSchema(new Schema([ - User::class => [ - SchemaInterface::ROLE => 'user', + AllUlid::class => [ + SchemaInterface::ROLE => 'all_ulid', SchemaInterface::DATABASE => 'default', - SchemaInterface::TABLE => 'users', - SchemaInterface::PRIMARY_KEY => 'ulid', - SchemaInterface::COLUMNS => ['ulid', 'foo_ulid'], - SchemaInterface::LISTENERS => [$listeners], + SchemaInterface::TABLE => 'all_ulids', + SchemaInterface::PRIMARY_KEY => 'id', + SchemaInterface::COLUMNS => ['id', 'ulid'], + SchemaInterface::LISTENERS => $listeners ? [$listeners] : [], SchemaInterface::SCHEMA => [], SchemaInterface::RELATIONS => [], SchemaInterface::TYPECAST => [ 'ulid' => [Ulid::class, 'create'], - 'foo_ulid' => [Ulid::class, 'create'], ], ], ])); @@ -98,10 +99,10 @@ public function setUp(): void parent::setUp(); $this->makeTable( - 'users', + 'all_ulids', [ - 'ulid' => 'string', - 'foo_ulid' => 'string,nullable', + 'id' => 'integer', + 'ulid' => 'ulid,nullable', ], ); } diff --git a/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php index 6c5f180..a40947a 100644 --- a/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php @@ -4,7 +4,7 @@ namespace Cycle\ORM\Entity\Behavior\Identifier\Tests\Functional\Driver\Common\Uuid; -use Cycle\ORM\Entity\Behavior\Identifier\Tests\Fixtures\Uuid\User; +use Cycle\ORM\Entity\Behavior\Identifier\Tests\Fixtures\Uuid\AllUuid; 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\Uuid1 as Uuid1Listener; @@ -14,134 +14,114 @@ use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid5 as Uuid5Listener; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid6 as Uuid6Listener; use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid7 as Uuid7Listener; -use Cycle\ORM\Entity\Behavior\Identifier\Uuid; +use Cycle\ORM\Entity\Behavior\Identifier\Uuid1; +use Cycle\ORM\Entity\Behavior\Identifier\Uuid2; +use Cycle\ORM\Entity\Behavior\Identifier\Uuid3; +use Cycle\ORM\Entity\Behavior\Identifier\Uuid4; +use Cycle\ORM\Entity\Behavior\Identifier\Uuid5; +use Cycle\ORM\Entity\Behavior\Identifier\Uuid6; +use Cycle\ORM\Entity\Behavior\Identifier\Uuid7; use Cycle\ORM\Heap\Heap; use Cycle\ORM\Schema; use Cycle\ORM\SchemaInterface; use Cycle\ORM\Select; use Ramsey\Identifier\Uuid\DceDomain; use Ramsey\Identifier\Uuid\NamespaceId; -use Ramsey\Identifier\Uuid\UntypedUuid; use Ramsey\Identifier\Uuid\UuidFactory; +use Ramsey\Identifier\Uuid\UuidV1; +use Ramsey\Identifier\Uuid\UuidV2; +use Ramsey\Identifier\Uuid\UuidV3; +use Ramsey\Identifier\Uuid\UuidV4; +use Ramsey\Identifier\Uuid\UuidV5; +use Ramsey\Identifier\Uuid\UuidV6; +use Ramsey\Identifier\Uuid\UuidV7; abstract class ListenerTest extends BaseTest { use TableTrait; - public static function nullableTrueDataProvider(): \Traversable + private UuidFactory $factory; + + public function testNullable(): void { - yield [ + $this->withListeners([ + Uuid1Listener::class, [ - Uuid1Listener::class, - [ - 'nullable' => true, - 'field' => 'optional_uuid', - 'node' => '00000fffffff', - 'clockSeq' => 0xffff, - ], + 'field' => 'uuid1', + 'nullable' => true, ], - ]; - yield [ + Uuid2Listener::class, [ - Uuid2Listener::class, - [ - 'nullable' => true, - 'field' => 'optional_uuid', - 'localDomain' => DceDomain::Person, - 'localIdentifier' => 12345678, - ], + 'field' => 'uuid2', + 'nullable' => true, ], - ]; - yield [ + Uuid3Listener::class, [ - Uuid3Listener::class, - [ - 'nullable' => true, - 'field' => 'optional_uuid', - 'namespace' => NamespaceId::Url, - 'name' => 'https://example.com/foo', - ], + 'field' => 'uuid3', + 'nullable' => true, ], - ]; - yield [ - [ - Uuid4Listener::class, - [ - 'nullable' => true, - 'field' => 'optional_uuid', - ], - ], - ]; - yield [ + Uuid4Listener::class, [ - Uuid5Listener::class, - [ - 'nullable' => true, - 'field' => 'optional_uuid', - 'namespace' => NamespaceId::Url, - 'name' => 'https://example.com/foo', - ], + 'field' => 'uuid4', + 'nullable' => true, ], - ]; - yield [ + Uuid5Listener::class, [ - Uuid6Listener::class, - [ - 'nullable' => true, - 'field' => 'optional_uuid', - 'node' => '00000fffffff', - 'clockSeq' => 0x1669, - ], + 'field' => 'uuid5', + 'nullable' => true, ], - ]; - yield [ + Uuid6Listener::class, [ - Uuid7Listener::class, - [ - 'nullable' => true, - 'field' => 'optional_uuid', - ], + 'field' => 'uuid6', + 'nullable' => true, ], - ]; - } - - public function testAssignManually(): void - { - $this->withListeners([ - Uuid4Listener::class, + Uuid7Listener::class, [ - 'field' => 'uuid', + 'field' => 'uuid7', + 'nullable' => true, ], ]); - $user = new User(); - $user->uuid = (new UuidFactory())->v4(); - $bytes = $user->uuid->toBytes(); + $entity = new AllUuid(); + $this->save($entity); - $this->save($user); - - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); $data = $select->fetchOne(); - $this->assertSame($bytes, $data->uuid->toBytes()); + $this->assertNull($data->uuid1); + $this->assertNull($data->uuid2); + $this->assertNull($data->uuid3); + $this->assertNull($data->uuid4); + $this->assertNull($data->uuid5); + $this->assertNull($data->uuid6); + $this->assertNull($data->uuid7); } - /** - * @dataProvider nullableTrueDataProvider - */ - public function testWithNullableTrue(array $listener): void + public function testAssignManually(): void { - $this->withListeners($listener); + $this->withListeners(); - $user = new User(); - $user->uuid = (new UuidFactory())->v4(); + $entity = new AllUuid(); + $entity->uuid1 = $this->factory->v1(); + $entity->uuid2 = $this->factory->v2(); + $entity->uuid3 = $this->factory->v3(NamespaceId::Url, 'https://cycle-orm.dev'); + $entity->uuid4 = $this->factory->v4(); + $entity->uuid5 = $this->factory->v5(NamespaceId::Url, 'https://cycle-orm.dev'); + $entity->uuid6 = $this->factory->v6(); + $entity->uuid7 = $this->factory->v7(); - $this->save($user); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); - $data = $select->fetchData(); + $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); + $data = $select->fetchOne(); - $this->assertNull($data[0]['optional_uuid']); + $this->assertSame($entity->uuid1->toString(), $data->uuid1->toString()); + $this->assertSame($entity->uuid2->toString(), $data->uuid2->toString()); + $this->assertSame($entity->uuid3->toString(), $data->uuid3->toString()); + $this->assertSame($entity->uuid4->toString(), $data->uuid4->toString()); + $this->assertSame($entity->uuid5->toString(), $data->uuid5->toString()); + $this->assertSame($entity->uuid6->toString(), $data->uuid6->toString()); + $this->assertSame($entity->uuid7->toString(), $data->uuid7->toString()); } public function testUuid1(): void @@ -149,21 +129,21 @@ public function testUuid1(): void $this->withListeners([ Uuid1Listener::class, [ - 'field' => 'uuid', + 'field' => 'uuid1', 'node' => '00000fffffff', 'clockSeq' => 0xffff, ], ]); - $user = new User(); - $this->save($user); + $entity = new AllUuid(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); $data = $select->fetchOne(); - $this->assertInstanceOf(UntypedUuid::class, $data->uuid); - $this->assertSame(1, $data->uuid->getVersion()->value); - $this->assertIsString($data->uuid->toString()); + $this->assertInstanceOf(UuidV1::class, $data->uuid1); + $this->assertSame(1, $data->uuid1->getVersion()->value); + $this->assertIsString($data->uuid1->toString()); } public function testUuid2(): void @@ -171,21 +151,21 @@ public function testUuid2(): void $this->withListeners([ Uuid2Listener::class, [ - 'field' => 'uuid', + 'field' => 'uuid2', 'localDomain' => DceDomain::Person, 'localIdentifier' => 12345678, ], ]); - $user = new User(); - $this->save($user); + $entity = new AllUuid(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); $data = $select->fetchOne(); - $this->assertInstanceOf(UntypedUuid::class, $data->uuid); - $this->assertSame(2, $data->uuid->getVersion()->value); - $this->assertIsString($data->uuid->toString()); + $this->assertInstanceOf(UuidV2::class, $data->uuid2); + $this->assertSame(2, $data->uuid2->getVersion()->value); + $this->assertIsString($data->uuid2->toString()); } public function testUuid3(): void @@ -193,21 +173,53 @@ public function testUuid3(): void $this->withListeners([ Uuid3Listener::class, [ - 'field' => 'uuid', + 'field' => 'uuid3', 'namespace' => NamespaceId::Url, 'name' => 'https://example.com/foo', ], ]); - $user = new User(); - $this->save($user); + $entity = new AllUuid(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); $data = $select->fetchOne(); - $this->assertInstanceOf(UntypedUuid::class, $data->uuid); - $this->assertSame(3, $data->uuid->getVersion()->value); - $this->assertIsString($data->uuid->toString()); + $this->assertInstanceOf(UuidV3::class, $data->uuid3); + $this->assertSame(3, $data->uuid3->getVersion()->value); + $this->assertIsString($data->uuid3->toString()); + } + + public function testUuid3ThrowsExceptionWhenNamespaceNotSpecified(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->withListeners([ + Uuid3Listener::class, + [ + 'field' => 'uuid3', + 'name' => 'https://example.com/foo', + ], + ]); + + $entity = new AllUuid(); + $this->save($entity); + } + + public function testUuid3ThrowsExceptionWhenNameNotSpecified(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->withListeners([ + Uuid3Listener::class, + [ + 'field' => 'uuid3', + 'namespace' => NamespaceId::Url, + ], + ]); + + $entity = new AllUuid(); + $this->save($entity); } public function testUuid4(): void @@ -215,19 +227,19 @@ public function testUuid4(): void $this->withListeners([ Uuid4Listener::class, [ - 'field' => 'uuid', + 'field' => 'uuid4', ], ]); - $user = new User(); - $this->save($user); + $entity = new AllUuid(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); $data = $select->fetchOne(); - $this->assertInstanceOf(UntypedUuid::class, $data->uuid); - $this->assertSame(4, $data->uuid->getVersion()->value); - $this->assertIsString($data->uuid->toString()); + $this->assertInstanceOf(UuidV4::class, $data->uuid4); + $this->assertSame(4, $data->uuid4->getVersion()->value); + $this->assertIsString($data->uuid4->toString()); } public function testUuid5(): void @@ -235,21 +247,53 @@ public function testUuid5(): void $this->withListeners([ Uuid5Listener::class, [ - 'field' => 'uuid', + 'field' => 'uuid5', 'namespace' => NamespaceId::Url, 'name' => 'https://example.com/foo', ], ]); - $user = new User(); - $this->save($user); + $entity = new AllUuid(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); $data = $select->fetchOne(); - $this->assertInstanceOf(UntypedUuid::class, $data->uuid); - $this->assertSame(5, $data->uuid->getVersion()->value); - $this->assertIsString($data->uuid->toString()); + $this->assertInstanceOf(UuidV5::class, $data->uuid5); + $this->assertSame(5, $data->uuid5->getVersion()->value); + $this->assertIsString($data->uuid5->toString()); + } + + public function testUuid5ThrowsExceptionWhenNamespaceNotSpecified(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->withListeners([ + Uuid5Listener::class, + [ + 'field' => 'uuid5', + 'name' => 'https://example.com/foo', + ], + ]); + + $entity = new AllUuid(); + $this->save($entity); + } + + public function testUuid5ThrowsExceptionWhenNameNotSpecified(): void + { + $this->expectException(\InvalidArgumentException::class); + + $this->withListeners([ + Uuid5Listener::class, + [ + 'field' => 'uuid5', + 'namespace' => NamespaceId::Url, + ], + ]); + + $entity = new AllUuid(); + $this->save($entity); } public function testUuid6(): void @@ -257,21 +301,21 @@ public function testUuid6(): void $this->withListeners([ Uuid6Listener::class, [ - 'field' => 'uuid', + 'field' => 'uuid6', 'node' => '00000fffffff', - 'clockSeq' => 0x1669, + 'clockSeq' => 0xffff, ], ]); - $user = new User(); - $this->save($user); + $entity = new AllUuid(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); $data = $select->fetchOne(); - $this->assertInstanceOf(UntypedUuid::class, $data->uuid); - $this->assertSame(6, $data->uuid->getVersion()->value); - $this->assertIsString($data->uuid->toString()); + $this->assertInstanceOf(UuidV6::class, $data->uuid6); + $this->assertSame(6, $data->uuid6->getVersion()->value); + $this->assertIsString($data->uuid6->toString()); } public function testUuid7(): void @@ -279,36 +323,41 @@ public function testUuid7(): void $this->withListeners([ Uuid7Listener::class, [ - 'field' => 'uuid', + 'field' => 'uuid7', ], ]); - $user = new User(); - $this->save($user); + $entity = new AllUuid(); + $this->save($entity); - $select = new Select($this->orm->with(heap: new Heap()), User::class); + $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); $data = $select->fetchOne(); - $this->assertInstanceOf(UntypedUuid::class, $data->uuid); - $this->assertSame(7, $data->uuid->getVersion()->value); - $this->assertIsString($data->uuid->toString()); + $this->assertInstanceOf(UuidV7::class, $data->uuid7); + $this->assertSame(7, $data->uuid7->getVersion()->value); + $this->assertIsString($data->uuid7->toString()); } - public function withListeners(array|string $listeners): void + public function withListeners(array|string|null $listeners = null): void { $this->withSchema(new Schema([ - User::class => [ - SchemaInterface::ROLE => 'user', + AllUuid::class => [ + SchemaInterface::ROLE => 'all_uuid', SchemaInterface::DATABASE => 'default', - SchemaInterface::TABLE => 'users', - SchemaInterface::PRIMARY_KEY => 'uuid', - SchemaInterface::COLUMNS => ['uuid', 'optional_uuid'], - SchemaInterface::LISTENERS => [$listeners], + SchemaInterface::TABLE => 'all_uuids', + SchemaInterface::PRIMARY_KEY => 'id', + SchemaInterface::COLUMNS => ['id', 'uuid1', 'uuid2', 'uuid3', 'uuid4', 'uuid5', 'uuid6', 'uuid7'], + SchemaInterface::LISTENERS => $listeners ? [$listeners] : [], SchemaInterface::SCHEMA => [], SchemaInterface::RELATIONS => [], SchemaInterface::TYPECAST => [ - 'uuid' => [Uuid::class, 'fromString'], - 'optional_uuid' => [Uuid::class, 'fromString'], + 'uuid1' => [Uuid1::class, 'create'], + 'uuid2' => [Uuid2::class, 'create'], + 'uuid3' => [Uuid3::class, 'create'], + 'uuid4' => [Uuid4::class, 'create'], + 'uuid5' => [Uuid5::class, 'create'], + 'uuid6' => [Uuid6::class, 'create'], + 'uuid7' => [Uuid7::class, 'create'], ], ], ])); @@ -319,11 +368,22 @@ public function setUp(): void { parent::setUp(); + Uuid3Listener::setDefaults(null, null); + Uuid5Listener::setDefaults(null, null); + + $this->factory = new UuidFactory(); + $this->makeTable( - 'users', + 'all_uuids', [ - 'uuid' => 'string', - 'optional_uuid' => 'string,nullable', + 'id' => 'integer', + 'uuid1' => 'uuid,nullable', + 'uuid2' => 'uuid,nullable', + 'uuid3' => 'uuid,nullable', + 'uuid4' => 'uuid,nullable', + 'uuid5' => 'uuid,nullable', + 'uuid6' => 'uuid,nullable', + 'uuid7' => 'uuid,nullable', ], ); } diff --git a/tests/Identifier/Functional/Driver/Common/Uuid/UuidTest.php b/tests/Identifier/Functional/Driver/Common/Uuid/UuidTest.php index 7bed336..f821c14 100644 --- a/tests/Identifier/Functional/Driver/Common/Uuid/UuidTest.php +++ b/tests/Identifier/Functional/Driver/Common/Uuid/UuidTest.php @@ -10,7 +10,9 @@ use Cycle\ORM\Entity\Behavior\Identifier\Tests\Fixtures\Uuid\Post; use Cycle\ORM\Entity\Behavior\Identifier\Tests\Fixtures\Uuid\User; use Cycle\ORM\Entity\Behavior\Identifier\Tests\Functional\Driver\Common\BaseTest; -use Cycle\ORM\Entity\Behavior\Identifier\Uuid; +use Cycle\ORM\Entity\Behavior\Identifier\Uuid1; +use Cycle\ORM\Entity\Behavior\Identifier\Uuid4; +use Cycle\ORM\Entity\Behavior\Identifier\Uuid7; use Cycle\ORM\Schema\GeneratedField; use Cycle\Schema\Registry; use Spiral\Attributes\AttributeReader; @@ -35,7 +37,7 @@ public function testColumnExist(ReaderInterface $reader): void $this->assertTrue($fields->has('uuid')); $this->assertTrue($fields->hasColumn('uuid')); $this->assertSame('uuid', $fields->get('uuid')->getType()); - $this->assertSame([Uuid::class, 'fromString'], $fields->get('uuid')->getTypecast()); + $this->assertSame([Uuid1::class, 'create'], $fields->get('uuid')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('uuid')->getGenerated()); $this->assertSame(1, $fields->count()); } @@ -52,7 +54,7 @@ public function testAddColumn(ReaderInterface $reader): void $this->assertTrue($fields->has('customUuid')); $this->assertTrue($fields->hasColumn('custom_uuid')); $this->assertSame('uuid', $fields->get('customUuid')->getType()); - $this->assertSame([Uuid::class, 'fromString'], $fields->get('customUuid')->getTypecast()); + $this->assertSame([Uuid4::class, 'create'], $fields->get('customUuid')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('customUuid')->getGenerated()); } @@ -68,25 +70,25 @@ public function testMultipleUuid(ReaderInterface $reader): void $this->assertTrue($fields->has('uuid')); $this->assertTrue($fields->hasColumn('uuid')); $this->assertSame('uuid', $fields->get('uuid')->getType()); - $this->assertSame([Uuid::class, 'fromString'], $fields->get('uuid')->getTypecast()); + $this->assertSame([Uuid1::class, 'create'], $fields->get('uuid')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('uuid')->getGenerated()); $this->assertTrue($fields->has('otherUuid')); $this->assertTrue($fields->hasColumn('other_uuid')); $this->assertSame('uuid', $fields->get('otherUuid')->getType()); - $this->assertSame([Uuid::class, 'fromString'], $fields->get('otherUuid')->getTypecast()); + $this->assertSame([Uuid1::class, 'create'], $fields->get('otherUuid')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('otherUuid')->getGenerated()); $this->assertTrue($fields->has('uuid7')); $this->assertTrue($fields->hasColumn('uuid7')); $this->assertSame('uuid', $fields->get('uuid7')->getType()); - $this->assertSame([Uuid::class, 'fromString'], $fields->get('uuid7')->getTypecast()); + $this->assertSame([Uuid7::class, 'create'], $fields->get('uuid7')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('uuid7')->getGenerated()); $this->assertTrue($fields->has('otherUuid7')); $this->assertTrue($fields->hasColumn('other_uuid7')); $this->assertSame('uuid', $fields->get('otherUuid7')->getType()); - $this->assertSame([Uuid::class, 'fromString'], $fields->get('otherUuid7')->getTypecast()); + $this->assertSame([Uuid7::class, 'create'], $fields->get('otherUuid7')->getTypecast()); $this->assertSame(GeneratedField::BEFORE_INSERT, $fields->get('otherUuid7')->getGenerated()); } @@ -102,7 +104,7 @@ public function testAddNullableColumn(ReaderInterface $reader): void $this->assertTrue($fields->has('notDefinedUuid')); $this->assertTrue($fields->hasColumn('not_defined_uuid')); $this->assertSame('uuid', $fields->get('notDefinedUuid')->getType()); - $this->assertSame([Uuid::class, 'fromString'], $fields->get('notDefinedUuid')->getTypecast()); + $this->assertSame([Uuid1::class, 'create'], $fields->get('notDefinedUuid')->getTypecast()); $this->assertTrue( $this->registry ->getTableSchema($this->registry->getEntity(NullableUuid::class)) diff --git a/tests/Identifier/Unit/Uuid2Test.php b/tests/Identifier/Unit/Uuid2Test.php index 8fe19b6..f929947 100644 --- a/tests/Identifier/Unit/Uuid2Test.php +++ b/tests/Identifier/Unit/Uuid2Test.php @@ -22,7 +22,7 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'uuid', - 'localDomain' => DceDomain::Person, + 'localDomain' => DceDomain::Person->value, 'localIdentifier' => null, 'node' => null, 'clockSeq' => null, @@ -40,7 +40,7 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_uuid', - 'localDomain' => DceDomain::Person, + 'localDomain' => DceDomain::Person->value, 'localIdentifier' => null, 'node' => null, 'clockSeq' => null, @@ -58,7 +58,7 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_uuid', - 'localDomain' => DceDomain::Person, + 'localDomain' => DceDomain::Person->value, 'localIdentifier' => 3, 'node' => null, 'clockSeq' => null, @@ -76,7 +76,7 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_uuid', - 'localDomain' => DceDomain::Person, + 'localDomain' => DceDomain::Person->value, 'localIdentifier' => 3, 'node' => 'bar', 'clockSeq' => null, @@ -85,7 +85,7 @@ public static function schemaDataProvider(): \Traversable ], ], ], - ['custom_uuid', null, 0, 3, 'bar'], + ['custom_uuid', null, DceDomain::Person, 3, 'bar'], ]; yield [ [ @@ -94,7 +94,7 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_uuid', - 'localDomain' => DceDomain::Group, + 'localDomain' => DceDomain::Group->value, 'localIdentifier' => 3, 'node' => 'bar', 'clockSeq' => 4, @@ -103,7 +103,7 @@ public static function schemaDataProvider(): \Traversable ], ], ], - ['custom_uuid', null, 1, 3, 'bar', 4], + ['custom_uuid', null, DceDomain::Group, 3, 'bar', 4], ]; yield [ [ @@ -112,7 +112,7 @@ public static function schemaDataProvider(): \Traversable ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'custom_uuid', - 'localDomain' => DceDomain::Org, + 'localDomain' => DceDomain::Org->value, 'localIdentifier' => 3, 'node' => 'bar', 'clockSeq' => 4, @@ -121,7 +121,7 @@ public static function schemaDataProvider(): \Traversable ], ], ], - ['custom_uuid', null, 2, 3, 'bar', 4, true], + ['custom_uuid', null, DceDomain::Org, 3, 'bar', 4, true], ]; } @@ -149,7 +149,7 @@ public function testModifySchemaWithDefaults(): void ListenerProvider::DEFINITION_CLASS => Listener::class, ListenerProvider::DEFINITION_ARGS => [ 'field' => 'uuid', - 'localDomain' => DceDomain::Person, + 'localDomain' => DceDomain::Person->value, 'localIdentifier' => null, 'node' => null, 'clockSeq' => null, diff --git a/tests/Identifier/Unit/Uuid3Test.php b/tests/Identifier/Unit/Uuid3Test.php index 28bf587..068f5ab 100644 --- a/tests/Identifier/Unit/Uuid3Test.php +++ b/tests/Identifier/Unit/Uuid3Test.php @@ -9,11 +9,28 @@ use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid3 as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; +use Ramsey\Identifier\Uuid\NamespaceId; final class Uuid3Test extends TestCase { public static function schemaDataProvider(): \Traversable { + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'uuid', + 'namespace' => null, + 'name' => null, + 'nullable' => false, + ], + ], + ], + ], + [], + ]; yield [ [ SchemaInterface::LISTENERS => [ @@ -28,7 +45,7 @@ public static function schemaDataProvider(): \Traversable ], ], ], - ['foo', 'bar'], + ['uuid', null, 'foo', 'bar'], ]; yield [ [ @@ -44,7 +61,7 @@ public static function schemaDataProvider(): \Traversable ], ], ], - ['foo', 'bar', 'custom_uuid'], + ['custom_uuid', null, 'foo', 'bar'], ]; yield [ [ @@ -60,7 +77,7 @@ public static function schemaDataProvider(): \Traversable ], ], ], - ['foo', 'bar', 'custom_uuid', null, true], + ['custom_uuid', null, 'foo', 'bar', true], ]; } @@ -75,4 +92,39 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Listener::setDefaults('foo', 'bar'); + + $args = ['uuid', null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'uuid', + 'namespace' => null, + 'name' => null, + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $uuid = new Uuid3(...$args); + $uuid->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + #[\Override] + protected function setUp(): void + { + Listener::setDefaults(null, null); + + parent::setUp(); + } } diff --git a/tests/Identifier/Unit/Uuid5Test.php b/tests/Identifier/Unit/Uuid5Test.php index b77ea73..c5184b5 100644 --- a/tests/Identifier/Unit/Uuid5Test.php +++ b/tests/Identifier/Unit/Uuid5Test.php @@ -14,6 +14,22 @@ final class Uuid5Test extends TestCase { public static function schemaDataProvider(): \Traversable { + yield [ + [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'uuid', + 'namespace' => null, + 'name' => null, + 'nullable' => false, + ], + ], + ], + ], + [], + ]; yield [ [ SchemaInterface::LISTENERS => [ @@ -28,7 +44,7 @@ public static function schemaDataProvider(): \Traversable ], ], ], - ['foo', 'bar'], + ['uuid', null, 'foo', 'bar'], ]; yield [ [ @@ -44,7 +60,7 @@ public static function schemaDataProvider(): \Traversable ], ], ], - ['foo', 'bar', 'custom_uuid'], + ['custom_uuid', null, 'foo', 'bar'], ]; yield [ [ @@ -60,7 +76,7 @@ public static function schemaDataProvider(): \Traversable ], ], ], - ['foo', 'bar', 'custom_uuid', null, true], + ['custom_uuid', null, 'foo', 'bar', true], ]; } @@ -75,4 +91,39 @@ public function testModifySchema(array $expected, array $args): void $this->assertSame($expected, $schema); } + + public function testModifySchemaWithDefaults(): void + { + Listener::setDefaults('foo', 'bar'); + + $args = ['uuid', null, null, null, false]; + + $expected = [ + SchemaInterface::LISTENERS => [ + [ + ListenerProvider::DEFINITION_CLASS => Listener::class, + ListenerProvider::DEFINITION_ARGS => [ + 'field' => 'uuid', + 'namespace' => null, + 'name' => null, + 'nullable' => false, + ], + ], + ], + ]; + + $schema = []; + $uuid = new Uuid5(...$args); + $uuid->modifySchema($schema); + + $this->assertSame($expected, $schema); + } + + #[\Override] + protected function setUp(): void + { + Listener::setDefaults(null, null); + + parent::setUp(); + } } From 2ecded9304e603109caf70669a58107a764060de Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Sun, 2 Nov 2025 18:40:00 +1100 Subject: [PATCH 46/51] Updated documentation --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 176d275..42f79af 100644 --- a/README.md +++ b/README.md @@ -259,6 +259,24 @@ class User } ``` +To avoid redundancy, default values can be set globally, allowing shared attributes across multiple entities. + +```php +use Cycle\ORM\Entity\Behavior\Identifier\Listener; + +Listener\SnowflakeGeneric::setDefaults($node, $epochOffset); +Listener\SnowflakeDiscord::setDefaults($workerId, $processId); +Listener\SnowflakeInstagram::setDefaults($shardId); +Listener\SnowflakeMastodon::setDefaults($tableName); +Listener\SnowflakeTwitter::setDefaults($machineId); + +Listener\Uuid1::setDefaults($node, $clockSeq); +Listener\Uuid2::setDefaults($localDomain, $localIdentifier, $node, $clockSeq); +Listener\Uuid3::setDefaults($namespace, $name); +Listener\Uuid5::setDefaults($namespace, $name); +Listener\Uuid6::setDefaults($node, $clockSeq); +```` + You can find more information about Entity behavior Identifier [here](https://cycle-orm.dev/docs/entity-behaviors-identifier). ## License: From 73c830e7d10340646077b8b5f588e324eb6a67d0 Mon Sep 17 00:00:00 2001 From: github-actions Date: Sun, 2 Nov 2025 07:40:49 +0000 Subject: [PATCH 47/51] style(php-cs-fixer): fix coding standards --- src/Listener/Uuid3.php | 2 -- tests/Identifier/Unit/Uuid3Test.php | 1 - 2 files changed, 3 deletions(-) diff --git a/src/Listener/Uuid3.php b/src/Listener/Uuid3.php index 906639f..dcd9587 100644 --- a/src/Listener/Uuid3.php +++ b/src/Listener/Uuid3.php @@ -35,8 +35,6 @@ public function __construct( /** * Set default values for UUIDv3 generation. * - * @param NamespaceId|Uuid|string|null $namespace - * @param string|null $name */ public static function setDefaults( NamespaceId|Uuid|string|null $namespace, diff --git a/tests/Identifier/Unit/Uuid3Test.php b/tests/Identifier/Unit/Uuid3Test.php index 068f5ab..7b60811 100644 --- a/tests/Identifier/Unit/Uuid3Test.php +++ b/tests/Identifier/Unit/Uuid3Test.php @@ -9,7 +9,6 @@ use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid3 as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; -use Ramsey\Identifier\Uuid\NamespaceId; final class Uuid3Test extends TestCase { From cb7c1c367f30a90cc84def48e182493084bf242d Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Mon, 3 Nov 2025 01:14:05 +1100 Subject: [PATCH 48/51] Removed superfluous PHP docs --- src/Listener/Uuid3.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Listener/Uuid3.php b/src/Listener/Uuid3.php index 906639f..225825f 100644 --- a/src/Listener/Uuid3.php +++ b/src/Listener/Uuid3.php @@ -34,9 +34,6 @@ public function __construct( /** * Set default values for UUIDv3 generation. - * - * @param NamespaceId|Uuid|string|null $namespace - * @param string|null $name */ public static function setDefaults( NamespaceId|Uuid|string|null $namespace, From e54123e90fa27dd022d743c358cee3a9090bb58b Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Mon, 3 Nov 2025 01:14:17 +1100 Subject: [PATCH 49/51] Removed unused imports --- tests/Identifier/Unit/Uuid3Test.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Identifier/Unit/Uuid3Test.php b/tests/Identifier/Unit/Uuid3Test.php index 068f5ab..7b60811 100644 --- a/tests/Identifier/Unit/Uuid3Test.php +++ b/tests/Identifier/Unit/Uuid3Test.php @@ -9,7 +9,6 @@ use Cycle\ORM\Entity\Behavior\Identifier\Listener\Uuid3 as Listener; use Cycle\ORM\SchemaInterface; use PHPUnit\Framework\TestCase; -use Ramsey\Identifier\Uuid\NamespaceId; final class Uuid3Test extends TestCase { From 1962700c98574c27e19b1e2794c8e2696ad6bdee Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Mon, 3 Nov 2025 01:14:53 +1100 Subject: [PATCH 50/51] Improved test structure for listener classes --- .../Driver/Common/Snowflake/ListenerTest.php | 417 +++++++----------- .../Driver/Common/Uuid/ListenerTest.php | 362 +++++++-------- 2 files changed, 326 insertions(+), 453 deletions(-) diff --git a/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php index 6e1b420..f35337f 100644 --- a/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Snowflake/ListenerTest.php @@ -36,6 +36,142 @@ abstract class ListenerTest extends BaseTest { use TableTrait; + public static function snowflakeGenerationDataProvider(): array + { + return [ + 'generic' => [ + 'listeners' => [ + SnowflakeGenericListener::class, + [ + 'field' => 'generic', + 'node' => 10, + 'epochOffset' => 1662744255000, + ], + ], + 'field' => 'generic', + 'expectedClass' => GenericSnowflake::class, + 'expectedLength' => 18, + ], + 'discord' => [ + 'listeners' => [ + SnowflakeDiscordListener::class, + [ + 'field' => 'discord', + 'workerId' => 10, + 'processId' => 20, + ], + ], + 'field' => 'discord', + 'expectedClass' => DiscordSnowflake::class, + 'expectedLength' => 19, + ], + 'discord-omit-process-id' => [ + 'listeners' => [ + SnowflakeDiscordListener::class, + [ + 'field' => 'discord', + 'workerId' => 10, + ], + ], + 'field' => 'discord', + 'expectedClass' => DiscordSnowflake::class, + 'expectedLength' => 19, + ], + 'instagram' => [ + 'listeners' => [ + SnowflakeInstagramListener::class, + [ + 'field' => 'instagram', + 'shardId' => 10, + ], + ], + 'field' => 'instagram', + 'expectedClass' => InstagramSnowflake::class, + 'expectedLength' => 19, + ], + 'mastodon' => [ + 'listeners' => [ + SnowflakeMastodonListener::class, + [ + 'field' => 'mastodon', + 'tableName' => 'foo', + ], + ], + 'field' => 'mastodon', + 'expectedClass' => MastodonSnowflake::class, + 'expectedLength' => 18, + ], + 'twitter' => [ + 'listeners' => [ + SnowflakeTwitterListener::class, + [ + 'field' => 'twitter', + 'machineId' => 10, + ], + ], + 'field' => 'twitter', + 'expectedClass' => TwitterSnowflake::class, + 'expectedLength' => 19, + ], + ]; + } + + public static function snowflakeExceptionDataProvider(): array + { + return [ + 'generic-node-minimum' => [ + 'listenerClass' => SnowflakeGenericListener::class, + 'defaults' => [-1, 1662744250000], + 'expectedException' => \InvalidArgumentException::class, + ], + 'generic-node-maximum' => [ + 'listenerClass' => SnowflakeGenericListener::class, + 'defaults' => [1024, 1662744250000], + 'expectedException' => \InvalidArgumentException::class, + ], + 'discord-worker-id-minimum' => [ + 'listenerClass' => SnowflakeDiscordListener::class, + 'defaults' => [-1, 30], + 'expectedException' => \InvalidArgumentException::class, + ], + 'discord-worker-id-maximum' => [ + 'listenerClass' => SnowflakeDiscordListener::class, + 'defaults' => [281474976710656, 30], + 'expectedException' => \InvalidArgumentException::class, + ], + 'discord-process-id-minimum' => [ + 'listenerClass' => SnowflakeDiscordListener::class, + 'defaults' => [20, -1], + 'expectedException' => \InvalidArgumentException::class, + ], + 'discord-process-id-maximum' => [ + 'listenerClass' => SnowflakeDiscordListener::class, + 'defaults' => [20, 281474976710656], + 'expectedException' => \InvalidArgumentException::class, + ], + 'instagram-shard-id-minimum' => [ + 'listenerClass' => SnowflakeInstagramListener::class, + 'defaults' => [-1], + 'expectedException' => \InvalidArgumentException::class, + ], + 'instagram-shard-id-maximum' => [ + 'listenerClass' => SnowflakeInstagramListener::class, + 'defaults' => [1024], + 'expectedException' => \InvalidArgumentException::class, + ], + 'twitter-machine-id-minimum' => [ + 'listenerClass' => SnowflakeTwitterListener::class, + 'defaults' => [-1], + 'expectedException' => \InvalidArgumentException::class, + ], + 'twitter-machine-id-maximum' => [ + 'listenerClass' => SnowflakeTwitterListener::class, + 'defaults' => [1024], + 'expectedException' => \InvalidArgumentException::class, + ], + ]; + } + public function testNullable(): void { $this->withListeners([ @@ -102,202 +238,16 @@ public function testAssignManually(): void $this->assertSame($entity->twitter->toString(), $data->twitter->toString()); } - public function testGenericSnowflake(): void - { - $this->withListeners([ - SnowflakeGenericListener::class, - [ - 'field' => 'generic', - 'node' => 10, - 'epochOffset' => 1662744255000, - ], - ]); - - $entity = new AllSnowflake(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(GenericSnowflake::class, $data->generic); - $this->assertIsString($data->generic->toString()); - $this->assertSame(18, \strlen($data->generic->toString())); - } - - public function testGenericDefaults(): void - { - SnowflakeGenericListener::setDefaults(20, 1662744250000); - - $this->withListeners([ - SnowflakeGenericListener::class, - [ - 'field' => 'generic', - ], - ]); - - $entity = new AllSnowflake(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(GenericSnowflake::class, $data->generic); - $this->assertIsString($data->generic->toString()); - $this->assertSame(18, \strlen($data->generic->toString())); - } - - public function testGenericNodeMinimumThrowsException(): void - { - $this->expectException(\InvalidArgumentException::class); - - SnowflakeGenericListener::setDefaults(-1, 1662744250000); - } - - public function testGenericNodeMaximumThrowsException(): void - { - $this->expectException(\InvalidArgumentException::class); - - SnowflakeGenericListener::setDefaults(1024, 1662744250000); - } - - public function testDiscordSnowflake(): void - { - $this->withListeners([ - SnowflakeDiscordListener::class, - [ - 'field' => 'discord', - 'workerId' => 10, - 'processId' => 20, - ], - ]); - - $entity = new AllSnowflake(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(DiscordSnowflake::class, $data->discord); - $this->assertIsString($data->discord->toString()); - $this->assertSame(19, \strlen($data->discord->toString())); - } - - public function testDiscordDefaults(): void - { - SnowflakeDiscordListener::setDefaults(20, 30); - - $this->withListeners([ - SnowflakeDiscordListener::class, - [ - 'field' => 'discord', - ], - ]); - - $entity = new AllSnowflake(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(DiscordSnowflake::class, $data->discord); - $this->assertIsString($data->discord->toString()); - $this->assertSame(19, \strlen($data->discord->toString())); - } - - public function testDiscordWorkerIdMinimumThrowsException(): void - { - $this->expectException(\InvalidArgumentException::class); - - SnowflakeDiscordListener::setDefaults(-1, 30); - } - - public function testDiscordWorkerIdMaximumThrowsException(): void - { - $this->expectException(\InvalidArgumentException::class); - - SnowflakeDiscordListener::setDefaults(281474976710656, 30); - } - - public function testDiscordProcessIdMinimumThrowsException(): void - { - $this->expectException(\InvalidArgumentException::class); - - SnowflakeDiscordListener::setDefaults(20, -1); - } - - public function testDiscordProcessIdMaximumThrowsException(): void - { - $this->expectException(\InvalidArgumentException::class); - - SnowflakeDiscordListener::setDefaults(20, 281474976710656); - } - - public function testInstagramSnowflake(): void - { - $this->withListeners([ - SnowflakeInstagramListener::class, - [ - 'field' => 'instagram', - 'shardId' => 10, - ], - ]); - - $entity = new AllSnowflake(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(InstagramSnowflake::class, $data->instagram); - $this->assertIsString($data->instagram->toString()); - $this->assertSame(19, \strlen($data->instagram->toString())); - } - - public function testInstagramDefaults(): void - { - SnowflakeInstagramListener::setDefaults(20); - - $this->withListeners([ - SnowflakeInstagramListener::class, - [ - 'field' => 'instagram', - ], - ]); - - $entity = new AllSnowflake(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(InstagramSnowflake::class, $data->instagram); - $this->assertIsString($data->instagram->toString()); - $this->assertSame(19, \strlen($data->instagram->toString())); - } - - public function testInstagramShardIdMinimumThrowsException(): void - { - $this->expectException(\InvalidArgumentException::class); - - SnowflakeInstagramListener::setDefaults(-1); - } - - public function testInstagramShardIdMaximumThrowsException(): void - { - $this->expectException(\InvalidArgumentException::class); - - SnowflakeInstagramListener::setDefaults(1024); - } - - public function testMastodonSnowflake(): void - { - $this->withListeners([ - SnowflakeMastodonListener::class, - [ - 'field' => 'mastodon', - 'tableName' => 'users', - ], - ]); + /** + * @dataProvider snowflakeGenerationDataProvider + */ + public function testSnowflakeGeneration( + array $listeners, + string $field, + string $expectedClass, + int $expectedLength, + ): void { + $this->withListeners($listeners); $entity = new AllSnowflake(); $this->save($entity); @@ -305,66 +255,25 @@ public function testMastodonSnowflake(): void $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); $data = $select->fetchOne(); - $this->assertInstanceOf(MastodonSnowflake::class, $data->mastodon); - $this->assertIsString($data->mastodon->toString()); - $this->assertSame(18, \strlen($data->mastodon->toString())); + $snowflake = $data->$field; + $this->assertInstanceOf($expectedClass, $snowflake); + $this->assertIsString($snowflake->toString()); + $this->assertSame($expectedLength, \strlen($snowflake->toString())); } - public function testTwitterSnowflake(): void - { - $this->withListeners([ - SnowflakeTwitterListener::class, - [ - 'field' => 'twitter', - 'machineId' => 10, - ], - ]); - - $entity = new AllSnowflake(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(TwitterSnowflake::class, $data->twitter); - $this->assertIsString($data->twitter->toString()); - $this->assertSame(19, \strlen($data->twitter->toString())); - } - - public function testTwitterDefaults(): void - { - SnowflakeTwitterListener::setDefaults(20); - - $this->withListeners([ - SnowflakeTwitterListener::class, - [ - 'field' => 'twitter', - ], - ]); - - $entity = new AllSnowflake(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllSnowflake::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(TwitterSnowflake::class, $data->twitter); - $this->assertIsString($data->twitter->toString()); - $this->assertSame(19, \strlen($data->twitter->toString())); - } - - public function testTwitterMachineIdMinimumThrowsException(): void - { - $this->expectException(\InvalidArgumentException::class); - - SnowflakeTwitterListener::setDefaults(-1); - } - - public function testTwitterMachineIdMaximumThrowsException(): void - { - $this->expectException(\InvalidArgumentException::class); - - SnowflakeTwitterListener::setDefaults(1024); + /** + * @dataProvider snowflakeExceptionDataProvider + */ + public function testSnowflakeException( + string $listenerClass, + array $defaults, + string $expectedException, + ): void { + $this->expectException($expectedException); + + if (\class_exists($listenerClass) && \method_exists($listenerClass, 'setDefaults')) { + $listenerClass::setDefaults(...$defaults); + } } public function withListeners(array|string|null $listeners = null): void diff --git a/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php b/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php index a40947a..d493fb3 100644 --- a/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php +++ b/tests/Identifier/Functional/Driver/Common/Uuid/ListenerTest.php @@ -42,6 +42,146 @@ abstract class ListenerTest extends BaseTest private UuidFactory $factory; + public static function uuidGenerationDataProvider(): array + { + return [ + 'uuid1' => [ + 'listeners' => [ + Uuid1Listener::class, + [ + 'field' => 'uuid1', + 'node' => '00000fffffff', + 'clockSeq' => 0xffff, + ], + ], + 'field' => 'uuid1', + 'expectedClass' => UuidV1::class, + 'expectedVersion' => 1, + ], + 'uuid2' => [ + 'listeners' => [ + Uuid2Listener::class, + [ + 'field' => 'uuid2', + 'localDomain' => DceDomain::Person, + 'localIdentifier' => 12345678, + 'node' => '3c1239b4f540', + ], + ], + 'field' => 'uuid2', + 'expectedClass' => UuidV2::class, + 'expectedVersion' => 2, + ], + 'uuid3' => [ + 'listeners' => [ + Uuid3Listener::class, + [ + 'field' => 'uuid3', + 'namespace' => NamespaceId::Url, + 'name' => 'https://example.com/foo', + ], + ], + 'field' => 'uuid3', + 'expectedClass' => UuidV3::class, + 'expectedVersion' => 3, + ], + 'uuid4' => [ + 'listeners' => [ + Uuid4Listener::class, + [ + 'field' => 'uuid4', + ], + ], + 'field' => 'uuid4', + 'expectedClass' => UuidV4::class, + 'expectedVersion' => 4, + ], + 'uuid5' => [ + 'listeners' => [ + Uuid5Listener::class, + [ + 'field' => 'uuid5', + 'namespace' => NamespaceId::Url, + 'name' => 'https://example.com/foo', + ], + ], + 'field' => 'uuid5', + 'expectedClass' => UuidV5::class, + 'expectedVersion' => 5, + ], + 'uuid6' => [ + 'listeners' => [ + Uuid6Listener::class, + [ + 'field' => 'uuid6', + 'node' => '00000fffffff', + 'clockSeq' => 0xffff, + ], + ], + 'field' => 'uuid6', + 'expectedClass' => UuidV6::class, + 'expectedVersion' => 6, + ], + 'uuid7' => [ + 'listeners' => [ + Uuid7Listener::class, + [ + 'field' => 'uuid7', + ], + ], + 'field' => 'uuid7', + 'expectedClass' => UuidV7::class, + 'expectedVersion' => 7, + ], + ]; + } + + public static function uuidExceptionDataProvider(): array + { + return [ + 'uuid3-undefined-namespace' => [ + 'listeners' => [ + Uuid3Listener::class, + [ + 'field' => 'uuid3', + 'name' => 'https://example.com/foo', + ], + ], + 'expectedException' => \InvalidArgumentException::class, + ], + 'uuid3-undefined-name' => [ + 'listeners' => [ + Uuid3Listener::class, + [ + 'field' => 'uuid3', + 'namespace' => NamespaceId::Url, + ], + ], + 'expectedException' => \InvalidArgumentException::class, + ], + 'uuid5-undefined-namespace' => [ + 'listeners' => [ + Uuid5Listener::class, + [ + 'field' => 'uuid5', + 'name' => 'https://example.com/foo', + ], + ], + 'expectedException' => \InvalidArgumentException::class, + ], + 'uuid5-undefined-name' => [ + 'listeners' => [ + Uuid5Listener::class, + [ + 'field' => 'uuid5', + 'namespace' => NamespaceId::Url, + ], + ], + 'expectedException' => \InvalidArgumentException::class, + ], + ]; + } + public function testNullable(): void { $this->withListeners([ @@ -124,134 +264,16 @@ public function testAssignManually(): void $this->assertSame($entity->uuid7->toString(), $data->uuid7->toString()); } - public function testUuid1(): void - { - $this->withListeners([ - Uuid1Listener::class, - [ - 'field' => 'uuid1', - 'node' => '00000fffffff', - 'clockSeq' => 0xffff, - ], - ]); - - $entity = new AllUuid(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(UuidV1::class, $data->uuid1); - $this->assertSame(1, $data->uuid1->getVersion()->value); - $this->assertIsString($data->uuid1->toString()); - } - - public function testUuid2(): void - { - $this->withListeners([ - Uuid2Listener::class, - [ - 'field' => 'uuid2', - 'localDomain' => DceDomain::Person, - 'localIdentifier' => 12345678, - ], - ]); - - $entity = new AllUuid(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(UuidV2::class, $data->uuid2); - $this->assertSame(2, $data->uuid2->getVersion()->value); - $this->assertIsString($data->uuid2->toString()); - } - - public function testUuid3(): void - { - $this->withListeners([ - Uuid3Listener::class, - [ - 'field' => 'uuid3', - 'namespace' => NamespaceId::Url, - 'name' => 'https://example.com/foo', - ], - ]); - - $entity = new AllUuid(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(UuidV3::class, $data->uuid3); - $this->assertSame(3, $data->uuid3->getVersion()->value); - $this->assertIsString($data->uuid3->toString()); - } - - public function testUuid3ThrowsExceptionWhenNamespaceNotSpecified(): void - { - $this->expectException(\InvalidArgumentException::class); - - $this->withListeners([ - Uuid3Listener::class, - [ - 'field' => 'uuid3', - 'name' => 'https://example.com/foo', - ], - ]); - - $entity = new AllUuid(); - $this->save($entity); - } - - public function testUuid3ThrowsExceptionWhenNameNotSpecified(): void - { - $this->expectException(\InvalidArgumentException::class); - - $this->withListeners([ - Uuid3Listener::class, - [ - 'field' => 'uuid3', - 'namespace' => NamespaceId::Url, - ], - ]); - - $entity = new AllUuid(); - $this->save($entity); - } - - public function testUuid4(): void - { - $this->withListeners([ - Uuid4Listener::class, - [ - 'field' => 'uuid4', - ], - ]); - - $entity = new AllUuid(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(UuidV4::class, $data->uuid4); - $this->assertSame(4, $data->uuid4->getVersion()->value); - $this->assertIsString($data->uuid4->toString()); - } - - public function testUuid5(): void - { - $this->withListeners([ - Uuid5Listener::class, - [ - 'field' => 'uuid5', - 'namespace' => NamespaceId::Url, - 'name' => 'https://example.com/foo', - ], - ]); + /** + * @dataProvider uuidGenerationDataProvider + */ + public function testUuidGeneration( + array $listeners, + string $field, + string $expectedClass, + int $expectedVersion, + ): void { + $this->withListeners($listeners); $entity = new AllUuid(); $this->save($entity); @@ -259,83 +281,25 @@ public function testUuid5(): void $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); $data = $select->fetchOne(); - $this->assertInstanceOf(UuidV5::class, $data->uuid5); - $this->assertSame(5, $data->uuid5->getVersion()->value); - $this->assertIsString($data->uuid5->toString()); - } - - public function testUuid5ThrowsExceptionWhenNamespaceNotSpecified(): void - { - $this->expectException(\InvalidArgumentException::class); - - $this->withListeners([ - Uuid5Listener::class, - [ - 'field' => 'uuid5', - 'name' => 'https://example.com/foo', - ], - ]); - - $entity = new AllUuid(); - $this->save($entity); + $uuid = $data->$field; + $this->assertInstanceOf($expectedClass, $uuid); + $this->assertSame($expectedVersion, $uuid->getVersion()->value); + $this->assertIsString($uuid->toString()); } - public function testUuid5ThrowsExceptionWhenNameNotSpecified(): void - { - $this->expectException(\InvalidArgumentException::class); - - $this->withListeners([ - Uuid5Listener::class, - [ - 'field' => 'uuid5', - 'namespace' => NamespaceId::Url, - ], - ]); - - $entity = new AllUuid(); - $this->save($entity); - } - - public function testUuid6(): void - { - $this->withListeners([ - Uuid6Listener::class, - [ - 'field' => 'uuid6', - 'node' => '00000fffffff', - 'clockSeq' => 0xffff, - ], - ]); - - $entity = new AllUuid(); - $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); - $data = $select->fetchOne(); + /** + * @dataProvider uuidExceptionDataProvider + */ + public function testUuidException( + array $listeners, + string $expectedException, + ): void { + $this->expectException($expectedException); - $this->assertInstanceOf(UuidV6::class, $data->uuid6); - $this->assertSame(6, $data->uuid6->getVersion()->value); - $this->assertIsString($data->uuid6->toString()); - } - - public function testUuid7(): void - { - $this->withListeners([ - Uuid7Listener::class, - [ - 'field' => 'uuid7', - ], - ]); + $this->withListeners($listeners); $entity = new AllUuid(); $this->save($entity); - - $select = new Select($this->orm->with(heap: new Heap()), AllUuid::class); - $data = $select->fetchOne(); - - $this->assertInstanceOf(UuidV7::class, $data->uuid7); - $this->assertSame(7, $data->uuid7->getVersion()->value); - $this->assertIsString($data->uuid7->toString()); } public function withListeners(array|string|null $listeners = null): void From 17fc49edb8441497ac7e46e0d5f34434bd62afb4 Mon Sep 17 00:00:00 2001 From: Adam Dyson Date: Mon, 3 Nov 2025 15:54:07 +1100 Subject: [PATCH 51/51] Improved documentation format and content --- README.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 42f79af..9bfaa47 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,8 @@ class User ## ULID Examples -**ULID (Universally Unique Lexicographically Sortable Identifier):** A 128-bit identifier designed for high uniqueness and lexicographical sortability. It combines a timestamp component with random data, allowing for ordered IDs that can be generated rapidly and are human-readable, making it ideal for databases and distributed systems. +### ULID (Universally Unique Lexicographically Sortable Identifier) +A 128-bit identifier designed for high uniqueness and lexicographical sortability. It combines a timestamp component with random data, allowing for ordered IDs that can be generated rapidly and are human-readable, making it ideal for databases and distributed systems. ```php use Cycle\Annotated\Annotation\Column; @@ -165,7 +166,8 @@ class User } ``` -**UUID Version 3 (Name-based, MD5):** Created by hashing a namespace identifier and name using MD5, resulting in a deterministic UUID based on input data. +### UUID Version 3 (Name-based, MD5) +Created by hashing a namespace identifier and name using MD5, resulting in a deterministic UUID based on input data. ```php use Cycle\Annotated\Annotation\Column; @@ -186,7 +188,8 @@ class User } ``` -**UUID Version 4 (Random):** Generated entirely from random or pseudo-random numbers, offering high unpredictability and uniqueness. +### UUID Version 4 (Random) +Generated entirely from random or pseudo-random numbers, offering high unpredictability and uniqueness. ```php use Cycle\Annotated\Annotation\Column; @@ -203,7 +206,8 @@ class User } ``` -**UUID Version 5 (Name-based, SHA-1):** Similar to version 3 but uses SHA-1 hashing, providing a different deterministic UUID based on namespace and name. +### UUID Version 5 (Name-based, SHA-1) +Similar to version 3 but uses SHA-1 hashing, providing a different deterministic UUID based on namespace and name. ```php use Cycle\Annotated\Annotation\Column; @@ -242,7 +246,8 @@ class User } ``` -**UUID Version 7 (Draft/Upcoming):** A newer proposal designed to incorporate sortable features based on Unix timestamp, enhancing performance in database indexing. +### UUID Version 7 (Draft/Upcoming) +A newer proposal designed to incorporate sortable features based on Unix timestamp, enhancing performance in database indexing. ```php use Cycle\Annotated\Annotation\Column; @@ -259,7 +264,15 @@ class User } ``` -To avoid redundancy, default values can be set globally, allowing shared attributes across multiple entities. +## Global Configuration + +Some listener classes provide static functions allowing you to define global default values for various attributes. This approach helps you to: + +* Initialize defaults at a suitable point in your application's lifecycle. +* Customize defaults dynamically based on environment-specific conditions. +* Minimize redundancy by setting shared attribute values once, instead of repeatedly specifying them across entities. + +**Sample code:** ```php use Cycle\ORM\Entity\Behavior\Identifier\Listener;