├── LICENSE ├── README.md ├── composer.json ├── phpbench.json └── src ├── Exception ├── BadMethodCall.php ├── CannotDetermineVersion.php ├── DceIdentifierNotFound.php ├── IdentifierException.php ├── InvalidArgument.php ├── MacAddressNotFound.php ├── MissingFunction.php └── NotComparable.php ├── NodeBasedUuid.php ├── Service ├── BytesGenerator │ ├── BytesGenerator.php │ ├── FixedBytesGenerator.php │ ├── MonotonicBytesGenerator.php │ └── RandomBytesGenerator.php ├── Cache │ └── InMemoryCache.php ├── Clock │ ├── ClockSequence.php │ ├── FrozenClock.php │ ├── FrozenClockSequence.php │ ├── GeneratorState.php │ ├── GeneratorStateCache.php │ ├── InvalidGeneratorState.php │ ├── MonotonicClockSequence.php │ ├── Precision.php │ ├── RandomClockSequence.php │ ├── Rfc4122ClockSequence.php │ └── SystemClock.php ├── Dce │ ├── Dce.php │ ├── StaticDce.php │ └── SystemDce.php ├── Nic │ ├── Nic.php │ ├── RandomNic.php │ ├── StaticNic.php │ └── SystemNic.php ├── Os │ ├── Os.php │ └── PhpOs.php └── Sequence │ ├── FrozenSequence.php │ ├── MonotonicSequence.php │ ├── RandomSequence.php │ ├── Sequence.php │ └── SequenceOverflow.php ├── Snowflake.php ├── Snowflake ├── DiscordSnowflake.php ├── DiscordSnowflakeFactory.php ├── Epoch.php ├── GenericSnowflake.php ├── GenericSnowflakeFactory.php ├── InstagramSnowflake.php ├── InstagramSnowflakeFactory.php ├── TwitterSnowflake.php ├── TwitterSnowflakeFactory.php └── Utility │ ├── Format.php │ ├── Mask.php │ ├── Standard.php │ ├── StandardFactory.php │ ├── Time.php │ └── Validation.php ├── SnowflakeFactory.php ├── TimeBasedUuid.php ├── TimeBasedUuidFactory.php ├── Ulid.php ├── Ulid ├── MaxUlid.php ├── NilUlid.php ├── Ulid.php ├── UlidFactory.php └── Utility │ ├── Format.php │ ├── Mask.php │ ├── Standard.php │ └── Validation.php ├── UlidFactory.php ├── Uuid.php ├── Uuid ├── DceDomain.php ├── MaxUuid.php ├── MicrosoftGuid.php ├── MicrosoftGuidFactory.php ├── NamespaceId.php ├── NilUuid.php ├── NonstandardUuid.php ├── UntypedUuid.php ├── Utility │ ├── Binary.php │ ├── Format.php │ ├── Mask.php │ ├── NodeBased.php │ ├── Standard.php │ ├── StandardFactory.php │ ├── Time.php │ ├── TimeBased.php │ └── Validation.php ├── UuidFactory.php ├── UuidV1.php ├── UuidV1Factory.php ├── UuidV2.php ├── UuidV2Factory.php ├── UuidV3.php ├── UuidV3Factory.php ├── UuidV4.php ├── UuidV4Factory.php ├── UuidV5.php ├── UuidV5Factory.php ├── UuidV6.php ├── UuidV6Factory.php ├── UuidV7.php ├── UuidV7Factory.php ├── UuidV8.php ├── UuidV8Factory.php ├── Variant.php └── Version.php └── UuidFactory.php /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2025 Ben Ramsey 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

ramsey/identifier

2 | 3 |

4 | A PHP library for generating and working with identifiers 5 |

6 | 7 |

8 | Source Code 9 | Download Package 10 | PHP Programming Language 11 | Read License 12 | Build Status 13 | Codecov Code Coverage 14 |

15 | 16 | ## About 17 | 18 | 23 | 24 | 25 | This project adheres to a [code of conduct](CODE_OF_CONDUCT.md). 26 | By participating in this project and its community, you are expected to 27 | uphold this code. 28 | 29 | ## Installation 30 | 31 | Install this package as a dependency using [Composer](https://getcomposer.org). 32 | 33 | ``` bash 34 | composer require ramsey/identifier 35 | ``` 36 | 37 | 51 | 52 | ## Contributing 53 | 54 | Contributions are welcome! To contribute, please familiarize yourself with 55 | [CONTRIBUTING.md](CONTRIBUTING.md). 56 | 57 | ## Coordinated Disclosure 58 | 59 | Keeping user information safe and secure is a top priority, and we welcome the 60 | contribution of external security researchers. If you believe you've found a 61 | security issue in software that is maintained in this repository, please read 62 | [SECURITY.md](SECURITY.md) for instructions on submitting a vulnerability report. 63 | 64 | ## Copyright and License 65 | 66 | The ramsey/identifier library is copyright © [Ben Ramsey](https://benramsey.com) 67 | and licensed for use under the terms of the 68 | MIT License (MIT). Please see [LICENSE](LICENSE) for more information. 69 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ramsey/identifier", 3 | "description": "A PHP library for generating and working with identifiers, including UUIDs, ULIDs, and Snowflakes", 4 | "license": "MIT", 5 | "type": "library", 6 | "keywords": [ 7 | "guid", 8 | "id", 9 | "identifier", 10 | "snowflake", 11 | "uid", 12 | "ulid", 13 | "uuid" 14 | ], 15 | "authors": [ 16 | { 17 | "name": "Ben Ramsey", 18 | "email": "ben@benramsey.com", 19 | "homepage": "https://benramsey.com" 20 | } 21 | ], 22 | "require": { 23 | "php-64bit": "^8.2", 24 | "brick/math": "^0.13.0", 25 | "identifier/identifier": "^0.3.0", 26 | "psr/clock": "^1.0", 27 | "psr/simple-cache": "^3.0" 28 | }, 29 | "require-dev": { 30 | "captainhook/captainhook": "^5.25", 31 | "captainhook/plugin-composer": "^5.3", 32 | "dealerdirect/phpcodesniffer-composer-installer": "^1.0", 33 | "ergebnis/composer-normalize": "^2.45", 34 | "hamcrest/hamcrest-php": "^2.0", 35 | "mockery/mockery": "^1.6", 36 | "php-parallel-lint/php-console-highlighter": "^1.0", 37 | "php-parallel-lint/php-parallel-lint": "^1.4", 38 | "phpbench/phpbench": "^1.4", 39 | "phpstan/extension-installer": "^1.4", 40 | "phpstan/phpstan": "^2.1", 41 | "phpstan/phpstan-mockery": "^2.0", 42 | "phpstan/phpstan-phpunit": "^2.0", 43 | "phpunit/phpunit": "^11.5 || ^12.0", 44 | "ramsey/coding-standard": "^2.3", 45 | "ramsey/composer-repl": "^1.5", 46 | "ramsey/conventional-commits": "^1.6", 47 | "roave/security-advisories": "dev-latest" 48 | }, 49 | "provide": { 50 | "identifier/identifier-implementation": "*" 51 | }, 52 | "suggest": { 53 | "psr/simple-cache-implementation": "To cache the system node (MAC address) for faster lookup" 54 | }, 55 | "minimum-stability": "dev", 56 | "prefer-stable": true, 57 | "autoload": { 58 | "psr-4": { 59 | "Ramsey\\Identifier\\": "src/" 60 | } 61 | }, 62 | "autoload-dev": { 63 | "psr-4": { 64 | "Ramsey\\Bench\\Identifier\\": "tests/bench/", 65 | "Ramsey\\Test\\Identifier\\": "tests/unit/" 66 | } 67 | }, 68 | "config": { 69 | "allow-plugins": { 70 | "captainhook/plugin-composer": true, 71 | "composer/package-versions-deprecated": true, 72 | "dealerdirect/phpcodesniffer-composer-installer": true, 73 | "ergebnis/composer-normalize": true, 74 | "phpstan/extension-installer": true, 75 | "ramsey/composer-repl": true 76 | }, 77 | "sort-packages": true 78 | }, 79 | "extra": { 80 | "captainhook": { 81 | "force-install": true 82 | }, 83 | "ramsey/conventional-commits": { 84 | "configFile": "conventional-commits.json" 85 | } 86 | }, 87 | "scripts": { 88 | "dev:analyze": [ 89 | "@dev:analyze:phpstan" 90 | ], 91 | "dev:analyze:phpstan": "phpstan analyse --ansi --memory-limit=1G", 92 | "dev:build:clean": "git clean -fX build/", 93 | "dev:lint": [ 94 | "@dev:lint:syntax", 95 | "@dev:lint:style" 96 | ], 97 | "dev:lint:fix": "phpcbf", 98 | "dev:lint:style": "phpcs --colors", 99 | "dev:lint:syntax": "parallel-lint --colors src/ tests/", 100 | "dev:test": [ 101 | "@dev:lint", 102 | "@dev:analyze", 103 | "@dev:test:unit" 104 | ], 105 | "dev:test:bench": "phpbench run --ansi --progress=dots --report=aggregate", 106 | "dev:test:bench:ci": "phpbench run --ansi --progress=none --report=ci --output=artifact-tab --output=artifact-html", 107 | "dev:test:bench:html": "phpbench run --ansi --progress=dots --report=ci --output=artifact-html", 108 | "dev:test:coverage:ci": "@php -d 'xdebug.mode=coverage' vendor/bin/phpunit --colors=always --coverage-text --coverage-clover build/coverage/clover.xml --coverage-cobertura build/coverage/cobertura.xml --coverage-crap4j build/coverage/crap4j.xml --coverage-xml build/coverage/coverage-xml --log-junit build/junit.xml", 109 | "dev:test:coverage:html": "@php -d 'xdebug.mode=coverage' vendor/bin/phpunit --colors=always --coverage-html build/coverage/coverage-html/", 110 | "dev:test:unit": "phpunit --colors=always", 111 | "test": "@dev:test" 112 | }, 113 | "scripts-descriptions": { 114 | "dev:analyze": "Runs all static analysis checks.", 115 | "dev:analyze:phpstan": "Runs the PHPStan static analyzer.", 116 | "dev:build:clean": "Cleans the build/ directory.", 117 | "dev:lint": "Runs all linting checks.", 118 | "dev:lint:fix": "Auto-fixes coding standards issues, if possible.", 119 | "dev:lint:style": "Checks for coding standards issues.", 120 | "dev:lint:syntax": "Checks for syntax errors.", 121 | "dev:test": "Runs linting, static analysis, and unit tests.", 122 | "dev:test:bench": "Runs PHPBench and displays aggregate report to the console.", 123 | "dev:test:bench:ci": "Runs PHPBench and generates CI reports.", 124 | "dev:test:bench:html": "Runs PHPBench and generates HTML reports.", 125 | "dev:test:coverage:ci": "Runs unit tests and generates CI coverage reports.", 126 | "dev:test:coverage:html": "Runs unit tests and generates HTML coverage report.", 127 | "dev:test:unit": "Runs unit tests.", 128 | "test": "Runs linting, static analysis, and unit tests." 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /phpbench.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./vendor/phpbench/phpbench/phpbench.schema.json", 3 | "runner.bootstrap": "vendor/autoload.php", 4 | "runner.path": "tests/bench", 5 | "runner.retry_threshold": 5, 6 | "runner.revs": 10, 7 | "runner.iterations": 5, 8 | "runner.warmup": 2, 9 | "report.generators": { 10 | "ci": { 11 | "generator": "expression", 12 | "break": ["benchmark"] 13 | } 14 | }, 15 | "report.outputs": { 16 | "artifact-html": { 17 | "renderer": "html", 18 | "path": "build/bench/phpbench.html", 19 | "title": "ramsey/identifier: PHPBench Results" 20 | }, 21 | "artifact-tab": { 22 | "renderer": "delimited", 23 | "file": "build/bench/phpbench.tab" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Exception/BadMethodCall.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Exception; 18 | 19 | use BadMethodCallException; 20 | 21 | /** 22 | * Thrown when attempting to call a method from an unsupported context (e.g., calling `getVersion()` on a 23 | * {@see \Ramsey\Identifier\Uuid\MaxUuid}). 24 | */ 25 | class BadMethodCall extends BadMethodCallException implements IdentifierException 26 | { 27 | } 28 | -------------------------------------------------------------------------------- /src/Exception/CannotDetermineVersion.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Exception; 18 | 19 | use LogicException; 20 | 21 | /** 22 | * Thrown when unable to determine the version of an untyped UUID 23 | */ 24 | class CannotDetermineVersion extends LogicException implements IdentifierException 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/Exception/DceIdentifierNotFound.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Exception; 18 | 19 | use RuntimeException; 20 | 21 | /** 22 | * Thrown when unable to find a suitable DCE identifier 23 | */ 24 | class DceIdentifierNotFound extends RuntimeException implements IdentifierException 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/Exception/IdentifierException.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Exception; 18 | 19 | use Identifier\Exception\IdentifierException as BaseIdentifierException; 20 | 21 | /** 22 | * Thrown when an exception occurs in ramsey/identifier 23 | */ 24 | interface IdentifierException extends BaseIdentifierException 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgument.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Exception; 18 | 19 | use Identifier\Exception\InvalidArgument as IdentifierInvalidArgument; 20 | use InvalidArgumentException; 21 | 22 | /** 23 | * Thrown when a method or function argument does not conform to validation requirements. 24 | */ 25 | class InvalidArgument extends InvalidArgumentException implements 26 | IdentifierException, 27 | IdentifierInvalidArgument 28 | { 29 | } 30 | -------------------------------------------------------------------------------- /src/Exception/MacAddressNotFound.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Exception; 18 | 19 | use RuntimeException; 20 | 21 | /** 22 | * Thrown when unable to find a suitable MAC address 23 | */ 24 | class MacAddressNotFound extends RuntimeException implements IdentifierException 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/Exception/MissingFunction.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Exception; 18 | 19 | use LogicException; 20 | 21 | /** 22 | * Thrown when attempting to use functionality that relies on a PHP function that is not supported by the current system. 23 | */ 24 | class MissingFunction extends LogicException implements IdentifierException 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/Exception/NotComparable.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Exception; 18 | 19 | use Identifier\Exception\NotComparable as IdentifierNotComparable; 20 | use RuntimeException; 21 | 22 | /** 23 | * Thrown when unable to compare values, e.g. because the other value is not of the proper type, etc. 24 | */ 25 | class NotComparable extends RuntimeException implements 26 | IdentifierException, 27 | IdentifierNotComparable 28 | { 29 | } 30 | -------------------------------------------------------------------------------- /src/NodeBasedUuid.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier; 18 | 19 | /** 20 | * A UUID that includes a node identifier (or MAC address). 21 | */ 22 | interface NodeBasedUuid extends Uuid 23 | { 24 | /** 25 | * Returns a string representation of the node (usually the host MAC address), encoded as hexadecimal characters. 26 | * 27 | * If the node has the multicast bit set, this indicates it was randomly generated, rather than identifying a host 28 | * machine. 29 | * 30 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.1 RFC 9562, section 5.1. UUID Version 1. 31 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.6 RFC 9562, section 5.6. UUID Version 6. 32 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-6.10 RFC 9562, section 6.10. UUIDs That Do Not Identify the Host. 33 | * 34 | * @return non-empty-string 35 | */ 36 | public function getNode(): string; 37 | } 38 | -------------------------------------------------------------------------------- /src/Service/BytesGenerator/BytesGenerator.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\BytesGenerator; 18 | 19 | use DateTimeInterface; 20 | 21 | /** 22 | * Generates bytes used to create identifiers 23 | */ 24 | interface BytesGenerator 25 | { 26 | /** 27 | * Generates an n-length string of bytes. 28 | * 29 | * @param positive-int $length The number of bytes to generate. 30 | * @param DateTimeInterface | null $dateTime An optional date-time instance to use when generating the bytes; not 31 | * all generators will need or use this parameter. 32 | * 33 | * @return non-empty-string 34 | */ 35 | public function bytes(int $length = 16, ?DateTimeInterface $dateTime = null): string; 36 | } 37 | -------------------------------------------------------------------------------- /src/Service/BytesGenerator/FixedBytesGenerator.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\BytesGenerator; 18 | 19 | use DateTimeInterface; 20 | 21 | use function intdiv; 22 | use function str_repeat; 23 | use function strlen; 24 | use function substr; 25 | 26 | /** 27 | * A generator that returns a pre-determined string of bytes. 28 | */ 29 | final readonly class FixedBytesGenerator implements BytesGenerator 30 | { 31 | private int $bytesLength; 32 | 33 | /** 34 | * @param non-empty-string $bytes 35 | */ 36 | public function __construct(private string $bytes) 37 | { 38 | $this->bytesLength = strlen($this->bytes); 39 | } 40 | 41 | public function bytes(int $length = 16, ?DateTimeInterface $dateTime = null): string 42 | { 43 | $bytes = str_repeat($this->bytes, intdiv($length, $this->bytesLength) + 1); 44 | 45 | /** @var non-empty-string */ 46 | return substr($bytes, 0, $length); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Service/BytesGenerator/RandomBytesGenerator.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\BytesGenerator; 18 | 19 | use DateTimeInterface; 20 | 21 | use function random_bytes; 22 | 23 | /** 24 | * A generator that uses PHP's built-in `random_bytes()` function to generate cryptographically secure random bytes. 25 | * 26 | * @link https://www.php.net/random_bytes random_bytes(). 27 | */ 28 | final class RandomBytesGenerator implements BytesGenerator 29 | { 30 | public function bytes(int $length = 16, ?DateTimeInterface $dateTime = null): string 31 | { 32 | return random_bytes($length); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Service/Clock/ClockSequence.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Clock; 18 | 19 | use DateTimeInterface; 20 | use Ramsey\Identifier\Service\Sequence\Sequence; 21 | use Ramsey\Identifier\Service\Sequence\SequenceOverflow; 22 | 23 | /** 24 | * Derives a clock sequence value, to avoid duplicates or collisions. 25 | * 26 | * From RFC 9562, section 5.1.: 27 | * 28 | * > UUIDv1 also features a clock sequence field that is used to help avoid duplicates that could arise when the clock 29 | * > is set backwards in time or if the Node ID changes. 30 | * > 31 | * > [...] 32 | * > 33 | * > If the clock is set backwards, or if it might have been set backwards (e.g., while the system was powered off), and 34 | * > the UUID generator cannot be sure that no UUIDs were generated with timestamps larger than the value to which the 35 | * > clock was set, then the clock sequence MUST be changed. If the previous value of the clock sequence is known, it 36 | * > MAY be incremented; otherwise it SHOULD be set to a random or high-quality pseudorandom value. 37 | * > 38 | * > Similarly, if the Node ID changes (e.g., because a network card has been moved between machines), setting the clock 39 | * > sequence to a random number minimizes the probability of a duplicate due to slight differences in the clock 40 | * > settings of the machines. If the value of the clock sequence associated with the changed Node ID were known, then 41 | * > the clock sequence MAY be incremented, but that is unlikely. 42 | * > 43 | * > The clock sequence MUST be originally (i.e., once in the lifetime of a system) initialized to a random number to 44 | * > minimize the correlation across systems. This provides maximum protection against Node IDs that may move or switch 45 | * > from system to system rapidly. The initial value MUST NOT be correlated to the Node ID. 46 | * 47 | * From RFC 9562, section 5.6.: 48 | * 49 | * > The clock sequence and node bits SHOULD be reset to a pseudorandom value for each new UUIDv6 generated; however, 50 | * > implementations MAY choose to retain the old clock sequence and MAC address behavior from Section 5.1. 51 | * 52 | * @link https://www.rfc-editor.org/rfc/rfc9562 RFC 9562 53 | */ 54 | interface ClockSequence extends Sequence 55 | { 56 | /** 57 | * {@inheritDoc} 58 | * 59 | * @param non-empty-string | null $state For a clock sequence, the state typically identifies the machine or node. 60 | * This may be the MAC address, or it may be some other identifier, according to the application's needs. 61 | * @param DateTimeInterface | null $dateTime The date-time value is used together with the state value to keep track 62 | * of the clock sequence. 63 | * 64 | * @return int<0, max> Clock sequence values are always greater than or equal to zero 65 | */ 66 | public function current(?string $state = null, ?DateTimeInterface $dateTime = null): int; 67 | 68 | /** 69 | * Calculates and returns the next clock sequence value. 70 | * 71 | * {@inheritDoc} 72 | * 73 | * This value may be the same as the previous clock sequence value if the state (i.e., the node) hasn't changed and 74 | * the date-time value is later than the previous date-time value. 75 | * 76 | * @param non-empty-string | null $state For a clock sequence, the state typically identifies the machine or node. 77 | * This may be the MAC address, or it may be some other identifier, according to the application's needs. 78 | * @param DateTimeInterface | null $dateTime The date-time value is used together with the state value to keep track 79 | * of the clock sequence. If this value is less than or equal to a previously used clock value, the sequence 80 | * should increment the clock sequence value, since the clock has been set backwards. 81 | * 82 | * @return int<0, max> Clock sequence values are always greater than or equal to zero 83 | * 84 | * @throws SequenceOverflow if the maximum clock sequence value is reached 85 | */ 86 | public function next(?string $state = null, ?DateTimeInterface $dateTime = null): int; 87 | } 88 | -------------------------------------------------------------------------------- /src/Service/Clock/FrozenClock.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Clock; 18 | 19 | use DateTimeImmutable; 20 | use Psr\Clock\ClockInterface as Clock; 21 | 22 | /** 23 | * A clock that always returns a pre-defined date-time. 24 | */ 25 | final readonly class FrozenClock implements Clock 26 | { 27 | /** 28 | * @param DateTimeImmutable $dateTime The date-time instance this service should always return. 29 | */ 30 | public function __construct(private DateTimeImmutable $dateTime) 31 | { 32 | } 33 | 34 | public function now(): DateTimeImmutable 35 | { 36 | return $this->dateTime; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Service/Clock/FrozenClockSequence.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Clock; 18 | 19 | use DateTimeInterface; 20 | 21 | /** 22 | * A clock sequence that returns a pre-determined value as the sequence and never changes. 23 | */ 24 | final readonly class FrozenClockSequence implements ClockSequence 25 | { 26 | /** 27 | * @param int<0, max> $value A pre-determined sequence value. 28 | */ 29 | public function __construct(private int $value) 30 | { 31 | } 32 | 33 | public function current(?string $state = null, ?DateTimeInterface $dateTime = null): int 34 | { 35 | return $this->value; 36 | } 37 | 38 | public function next(?string $state = null, ?DateTimeInterface $dateTime = null): int 39 | { 40 | return $this->value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Service/Clock/GeneratorState.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Clock; 18 | 19 | /** 20 | * A value object for storing and passing the generator state for clock sequences. 21 | * 22 | * @internal 23 | */ 24 | final class GeneratorState 25 | { 26 | /** 27 | * @param non-empty-string $node 28 | * @param int<0, max> $sequence 29 | */ 30 | public function __construct( 31 | public string $node, 32 | public int $sequence, 33 | public int $timestamp, 34 | ) { 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Service/Clock/GeneratorStateCache.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Clock; 18 | 19 | use DateTimeInterface; 20 | 21 | use function random_int; 22 | 23 | use const PHP_INT_MAX; 24 | 25 | trait GeneratorStateCache 26 | { 27 | /** 28 | * @var int<0, max> | null 29 | */ 30 | private readonly ?int $initialValue; 31 | 32 | private bool $initialValueUsed = false; 33 | 34 | /** 35 | * @param non-empty-string $state 36 | */ 37 | private function getGeneratorStateFromCache( 38 | string $cacheKey, 39 | string $state, 40 | DateTimeInterface $dateTime, 41 | ): GeneratorState { 42 | $generatorState = $this->cache->get($cacheKey); 43 | 44 | if ($generatorState === null) { 45 | $generatorState = new GeneratorState( 46 | node: $state, 47 | sequence: $this->initializeValue(), 48 | timestamp: (int) $dateTime->format(Precision::Microsecond->value), 49 | ); 50 | } 51 | 52 | if (!$generatorState instanceof GeneratorState) { 53 | throw new InvalidGeneratorState('The generator state must be an instance of ' . GeneratorState::class); 54 | } 55 | 56 | return $generatorState; 57 | } 58 | 59 | /** 60 | * @return int<0, max> 61 | */ 62 | private function initializeValue(): int 63 | { 64 | if ($this->initialValue !== null && !$this->initialValueUsed) { 65 | $this->initialValueUsed = true; 66 | 67 | return $this->initialValue; 68 | } 69 | 70 | return random_int(0, PHP_INT_MAX); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Service/Clock/InvalidGeneratorState.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Clock; 18 | 19 | use Ramsey\Identifier\Exception\IdentifierException; 20 | use RuntimeException; 21 | 22 | /** 23 | * Thrown when the generator state is corrupt or otherwise invalid 24 | */ 25 | class InvalidGeneratorState extends RuntimeException implements IdentifierException 26 | { 27 | } 28 | -------------------------------------------------------------------------------- /src/Service/Clock/MonotonicClockSequence.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Clock; 18 | 19 | use DateTimeInterface; 20 | use Psr\Clock\ClockInterface; 21 | use Psr\SimpleCache\CacheInterface; 22 | use Ramsey\Identifier\Service\Cache\InMemoryCache; 23 | use Ramsey\Identifier\Service\Nic\Nic; 24 | use Ramsey\Identifier\Service\Nic\RandomNic; 25 | 26 | use const PHP_INT_MAX; 27 | 28 | /** 29 | * A clock sequence that always increases. 30 | * 31 | * @link https://en.wikipedia.org/wiki/Monotonic_function "Monotonic function" on Wikipedia 32 | */ 33 | final class MonotonicClockSequence implements ClockSequence 34 | { 35 | use GeneratorStateCache; 36 | 37 | /** 38 | * The cache key is generated from the Adler-32 checksum of this class name. 39 | * 40 | * ``` 41 | * hash('adler32', MonotonicClockSequence::class); 42 | * ``` 43 | */ 44 | private const CACHE_KEY = '__ramsey_id_4cdb157d'; 45 | 46 | /** 47 | * @var non-empty-string 48 | */ 49 | private readonly string $defaultState; 50 | 51 | /** 52 | * @param int<0, max> | null $initialValue An initial clock sequence value; if not provided, it is randomly generated 53 | * @param Nic $nic The system NIC, for maintaining state; defaults to {@see RandomNic} 54 | * @param ClockInterface $clock A clock to use for determining state; defaults to {@see SystemClock} 55 | * @param CacheInterface $cache A cache for storing the sequence and maintaining state 56 | */ 57 | public function __construct( 58 | ?int $initialValue = null, 59 | Nic $nic = new RandomNic(), 60 | private readonly ClockInterface $clock = new SystemClock(), 61 | private readonly CacheInterface $cache = new InMemoryCache(), 62 | private readonly Precision $precision = Precision::Millisecond, 63 | ) { 64 | $this->initialValue = $initialValue; 65 | $this->defaultState = $nic->address(); 66 | } 67 | 68 | public function current(?string $state = null, ?DateTimeInterface $dateTime = null): int 69 | { 70 | return $this->getGeneratorState($state, $dateTime, false)->sequence; 71 | } 72 | 73 | public function next(?string $state = null, ?DateTimeInterface $dateTime = null): int 74 | { 75 | return $this->getGeneratorState($state, $dateTime, true)->sequence; 76 | } 77 | 78 | /** 79 | * @param non-empty-string | null $state 80 | */ 81 | private function getGeneratorState(?string $state, ?DateTimeInterface $dateTime, bool $increment): GeneratorState 82 | { 83 | $dateTime = $dateTime ?? $this->clock->now(); 84 | $state = $state ?? $this->defaultState; 85 | 86 | $cacheKey = $this->getGeneratorStateCacheKey($state, $dateTime); 87 | $generatorState = $this->getGeneratorStateFromCache($cacheKey, $state, $dateTime); 88 | 89 | if ($increment) { 90 | // If the sequence is at the max value, roll it over to zero. 91 | if ($generatorState->sequence === PHP_INT_MAX) { 92 | $generatorState->sequence = 0; 93 | } else { 94 | $generatorState->sequence = $generatorState->sequence + 1; 95 | } 96 | } 97 | 98 | $this->cache->set($cacheKey, $generatorState); 99 | 100 | return $generatorState; 101 | } 102 | 103 | /** 104 | * @param non-empty-string $state 105 | */ 106 | private function getGeneratorStateCacheKey(string $state, DateTimeInterface $dateTime): string 107 | { 108 | return self::CACHE_KEY . '|' . $state . '|' . $dateTime->format($this->precision->value); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Service/Clock/Precision.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Clock; 18 | 19 | /** 20 | * Precision formats for clock sequences 21 | */ 22 | enum Precision: string 23 | { 24 | case Microsecond = 'Uu'; 25 | case Millisecond = 'Uv'; 26 | } 27 | -------------------------------------------------------------------------------- /src/Service/Clock/RandomClockSequence.php: -------------------------------------------------------------------------------- 1 | sequence = new RandomSequence(min: 0); 20 | } 21 | 22 | public function current(?string $state = null, ?DateTimeInterface $dateTime = null): int 23 | { 24 | /** @var int<0, max> */ 25 | return $this->sequence->current(); 26 | } 27 | 28 | public function next(?string $state = null, ?DateTimeInterface $dateTime = null): int 29 | { 30 | /** @var int<0, max> */ 31 | return $this->sequence->next(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Service/Clock/Rfc4122ClockSequence.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Clock; 18 | 19 | use DateTimeInterface; 20 | use Psr\Clock\ClockInterface; 21 | use Psr\SimpleCache\CacheInterface; 22 | use Ramsey\Identifier\Service\Cache\InMemoryCache; 23 | use Ramsey\Identifier\Service\Nic\Nic; 24 | use Ramsey\Identifier\Service\Nic\RandomNic; 25 | 26 | use const PHP_INT_MAX; 27 | 28 | /** 29 | * Calculates the clock sequence value, according to the algorithm defined in RFC 4122. 30 | * 31 | * RFC 4122, section 4.2.1 defines a basic algorithm for generating a clock sequence. It describes the algorithm as 32 | * "simple, correct, and inefficient." 33 | * 34 | * > * Obtain a system-wide global lock 35 | * > * From a system-wide shared stable store (e.g., a file), read the UUID generator state: the values of the timestamp, clock sequence, and node ID used to generate the last UUID. 36 | * > * Get the current time as a 60-bit count of 100-nanosecond intervals since 00:00:00.00, 15 October 1582. 37 | * > * Get the current node ID. 38 | * > * If the state was unavailable (e.g., non-existent or corrupted), or the saved node ID is different than the current node ID, generate a random clock sequence value. 39 | * > * If the state was available, but the saved timestamp is later than the current timestamp, increment the clock sequence value. 40 | * > * Save the state (current timestamp, clock sequence, and node ID) back to the stable store. 41 | * > * Release the global lock. 42 | * 43 | * The implementation in this class deviates from the algorithm in a few notable ways: 44 | * 45 | * * It does not acquire a system-wide global lock. 46 | * * The system-wide shared stable store is possible through use of a cache, which you may provide to the constructor. 47 | * * This implementation uses microsecond time precision instead of 100-nanosecond interval time precision. 48 | * 49 | * WARNING: Since this does not acquire a global lock, race conditions could occur when fetching or storing data to the 50 | * cache. If needed, you may create your own cache implementation that resolves these shortcomings. 51 | * 52 | * NOTE: The algorithm defined in RFC 4122 was not included in RFC 9562. 53 | * 54 | * @link https://www.rfc-editor.org/rfc/rfc4122#section-4.2.1 RFC 4122, 4.2.1. Basic Algorithm 55 | */ 56 | final class Rfc4122ClockSequence implements ClockSequence 57 | { 58 | use GeneratorStateCache; 59 | 60 | /** 61 | * The cache key is generated from the Adler-32 checksum of this class name. 62 | * 63 | * ``` 64 | * hash('adler32', Rfc4122ClockSequence::class); 65 | * ``` 66 | */ 67 | private const CACHE_KEY = '__ramsey_id_122f13ab'; 68 | 69 | /** 70 | * @var non-empty-string 71 | */ 72 | private readonly string $defaultState; 73 | 74 | /** 75 | * @param int<0, max> | null $initialValue An initial clock sequence value; if not provided, it is randomly generated 76 | * @param Nic $nic The system NIC, for maintaining state; defaults to {@see RandomNic} 77 | * @param ClockInterface $clock A clock to use for determining state; defaults to {@see SystemClock} 78 | * @param CacheInterface $cache A cache for storing the sequence and maintaining state 79 | */ 80 | public function __construct( 81 | ?int $initialValue = null, 82 | Nic $nic = new RandomNic(), 83 | private readonly ClockInterface $clock = new SystemClock(), 84 | private readonly CacheInterface $cache = new InMemoryCache(), 85 | ) { 86 | $this->initialValue = $initialValue; 87 | $this->defaultState = $nic->address(); 88 | } 89 | 90 | public function current(?string $state = null, ?DateTimeInterface $dateTime = null): int 91 | { 92 | return $this->getGeneratorState($state, $dateTime)->sequence; 93 | } 94 | 95 | public function next(?string $state = null, ?DateTimeInterface $dateTime = null): int 96 | { 97 | return $this->getGeneratorState($state, $dateTime)->sequence; 98 | } 99 | 100 | /** 101 | * @param non-empty-string | null $state 102 | */ 103 | private function getGeneratorState(?string $state, ?DateTimeInterface $dateTime): GeneratorState 104 | { 105 | $dateTime = $dateTime ?? $this->clock->now(); 106 | $state = $state ?? $this->defaultState; 107 | 108 | $cacheKey = $this->getGeneratorStateCacheKey($state); 109 | $generatorState = $this->getGeneratorStateFromCache($cacheKey, $state, $dateTime); 110 | 111 | // If the state timestamp is later than the current, the clock was set backward, and we must increment the sequence. 112 | if ($generatorState->timestamp > (int) $dateTime->format(Precision::Microsecond->value)) { 113 | // If the sequence is at the max value, roll it over to zero. 114 | if ($generatorState->sequence === PHP_INT_MAX) { 115 | $generatorState->sequence = 0; 116 | } else { 117 | $generatorState->sequence++; 118 | } 119 | } 120 | 121 | $generatorState->timestamp = (int) $dateTime->format(Precision::Microsecond->value); 122 | 123 | $this->cache->set($cacheKey, $generatorState); 124 | 125 | return $generatorState; 126 | } 127 | 128 | private function getGeneratorStateCacheKey(string $state): string 129 | { 130 | return self::CACHE_KEY . '|' . $state; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Service/Clock/SystemClock.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Clock; 18 | 19 | use DateTimeImmutable; 20 | use Psr\Clock\ClockInterface as Clock; 21 | 22 | /** 23 | * A clock that always returns the current system time 24 | */ 25 | final class SystemClock implements Clock 26 | { 27 | public function now(): DateTimeImmutable 28 | { 29 | return new DateTimeImmutable('now'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Service/Dce/Dce.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Dce; 18 | 19 | use Ramsey\Identifier\Exception\DceIdentifierNotFound; 20 | use Ramsey\Identifier\Uuid\UuidV2; 21 | 22 | /** 23 | * Provides system group, user, and organization identifiers for use with Distributed Computing Environment (DCE) 24 | * specifications. 25 | * 26 | * @link https://publications.opengroup.org/c311 DCE 1.1: Authentication and Security Services. 27 | * @see UuidV2 28 | */ 29 | interface Dce 30 | { 31 | /** 32 | * Returns a group identifier (i.e., GID) for the system. 33 | * 34 | * @link https://en.wikipedia.org/wiki/Group_identifier Group identifier. 35 | * 36 | * @return int<0, max> 37 | * 38 | * @throws DceIdentifierNotFound when unable to find a group identifier. 39 | */ 40 | public function groupId(): int; 41 | 42 | /** 43 | * Returns an organization identifier. 44 | * 45 | * @return int<0, max> 46 | * 47 | * @throws DceIdentifierNotFound when unable to find an organization identifier. 48 | */ 49 | public function orgId(): int; 50 | 51 | /** 52 | * Returns a user identifier (i.e., UID) for the system. 53 | * 54 | * @link https://en.wikipedia.org/wiki/User_identifier User identifier. 55 | * 56 | * @return int<0, max> 57 | * 58 | * @throws DceIdentifierNotFound when unable to find a user identifier. 59 | */ 60 | public function userId(): int; 61 | } 62 | -------------------------------------------------------------------------------- /src/Service/Dce/StaticDce.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Dce; 18 | 19 | use Ramsey\Identifier\Exception\DceIdentifierNotFound; 20 | 21 | use function sprintf; 22 | 23 | /** 24 | * Provides pre-determined user, group, and organization IDs for generating DCE Security (version 2) UUIDs. 25 | */ 26 | final readonly class StaticDce implements Dce 27 | { 28 | /** 29 | * Constructs a static DCE service. 30 | * 31 | * Each parameter is optional, so you may only provide the one you intend to use. However, when attempting to use a 32 | * method for a property not provided, this class will throw a {@see DceIdentifierNotFound}. 33 | * 34 | * @param int<0, max> | null $userId An optional user identifier, or UID. 35 | * @param int<0, max> | null $groupId An optional group identifier, or GID. 36 | * @param int<0, max> | null $orgId An optional organization identifier. 37 | */ 38 | public function __construct( 39 | private ?int $userId = null, 40 | private ?int $groupId = null, 41 | private ?int $orgId = null, 42 | ) { 43 | } 44 | 45 | /** 46 | * @throws DceIdentifierNotFound if a group identifier was not provided upon instantiation. 47 | */ 48 | public function groupId(): int 49 | { 50 | if ($this->groupId === null) { 51 | throw new DceIdentifierNotFound(sprintf( 52 | 'To use the group identifier, you must set $groupId when instantiating %s', 53 | self::class, 54 | )); 55 | } 56 | 57 | return $this->groupId; 58 | } 59 | 60 | /** 61 | * @throws DceIdentifierNotFound if an org identifier was not provided upon instantiation. 62 | */ 63 | public function orgId(): int 64 | { 65 | if ($this->orgId === null) { 66 | throw new DceIdentifierNotFound(sprintf( 67 | 'To use the org identifier, you must set $orgId when instantiating %s', 68 | self::class, 69 | )); 70 | } 71 | 72 | return $this->orgId; 73 | } 74 | 75 | /** 76 | * @throws DceIdentifierNotFound if a user identifier was not provided upon instantiation. 77 | */ 78 | public function userId(): int 79 | { 80 | if ($this->userId === null) { 81 | throw new DceIdentifierNotFound(sprintf( 82 | 'To use the user identifier, you must set $userId when instantiating %s', 83 | self::class, 84 | )); 85 | } 86 | 87 | return $this->userId; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Service/Nic/Nic.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Nic; 18 | 19 | /** 20 | * Derives a MAC address from a network interface controller (NIC). 21 | * 22 | * @link https://en.wikipedia.org/wiki/MAC_address MAC address. 23 | */ 24 | interface Nic 25 | { 26 | /** 27 | * Returns a MAC address as a 12-character hexadecimal string. 28 | * 29 | * This value must not include any separator characters. If not using the MAC address of the host where the 30 | * identifier is created, the value should set the multicast bit. See RFC 9562, section 6.10, for more details. 31 | * 32 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-6.10 RFC 9562, section 6.10. UUIDs That Do Not Identify the Host. 33 | * 34 | * @return non-empty-string 35 | */ 36 | public function address(): string; 37 | } 38 | -------------------------------------------------------------------------------- /src/Service/Nic/RandomNic.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Nic; 18 | 19 | use Psr\SimpleCache\CacheException; 20 | use Psr\SimpleCache\CacheInterface; 21 | use Ramsey\Identifier\Exception\MacAddressNotFound; 22 | 23 | use function random_int; 24 | use function sprintf; 25 | 26 | /** 27 | * A NIC that generates a random MAC address and sets the multicast bit, according to RFC 9562, section 6.10. The 28 | * address is stored and reused for each call to address() within the same process. If a cache is provided, the same 29 | * address is used across processes. 30 | * 31 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-6.10 RFC 9562, section 6.10. UUIDs That Do Not Identify the Host 32 | */ 33 | final class RandomNic implements Nic 34 | { 35 | /** 36 | * The cache key is generated from the Adler-32 checksum of this class name. 37 | * 38 | * ``` 39 | * hash('adler32', RandomNic::class); 40 | * ``` 41 | */ 42 | private const CACHE_KEY = '__ramsey_id_33d80f4b'; 43 | 44 | /** 45 | * The address, stored statically for better performance. 46 | * 47 | * @var non-empty-string | null 48 | */ 49 | private static ?string $address = null; 50 | 51 | /** 52 | * @param CacheInterface | null $cache An optional PSR-16 cache instance to cache the address for faster lookups. 53 | * Be aware that use of a centralized cache might have unintended consequences if you wish to use 54 | * machine-specific addresses. If you wish for machine-specific addresses, use of a machine-local cache, such as 55 | * APCu, is preferable. 56 | */ 57 | public function __construct(private readonly ?CacheInterface $cache = null) 58 | { 59 | } 60 | 61 | public function address(): string 62 | { 63 | if (self::$address === null) { 64 | try { 65 | self::$address = $this->getAddressFromCache(); 66 | } catch (CacheException $cacheException) { 67 | throw new MacAddressNotFound( 68 | message: 'Unable to retrieve MAC address from cache', 69 | previous: $cacheException, 70 | ); 71 | } 72 | } 73 | 74 | return self::$address; 75 | } 76 | 77 | /** 78 | * @return non-empty-string 79 | * 80 | * @throws CacheException 81 | */ 82 | private function getAddressFromCache(): string 83 | { 84 | /** @var string | null $address */ 85 | $address = $this->cache?->get(self::CACHE_KEY); 86 | 87 | if ($address === null || $address === '') { 88 | $address = $this->generateAddress(); 89 | $this->cache?->set(self::CACHE_KEY, $address); 90 | } 91 | 92 | return $address; 93 | } 94 | 95 | /** 96 | * @return non-empty-string 97 | */ 98 | public function generateAddress(): string 99 | { 100 | /** @var non-empty-string */ 101 | return sprintf('%012x', random_int(0, 0xffffffffffff) | 0x010000000000); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Service/Nic/StaticNic.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Nic; 18 | 19 | use Ramsey\Identifier\Exception\InvalidArgument; 20 | use Ramsey\Identifier\Uuid\Utility\Mask; 21 | 22 | use function bin2hex; 23 | use function dechex; 24 | use function hex2bin; 25 | use function is_int; 26 | use function pack; 27 | use function sprintf; 28 | use function strlen; 29 | use function strspn; 30 | use function unpack; 31 | 32 | /** 33 | * A NIC that provides a pre-determined MAC address and sets the multicast bit, according to RFC 9562, section 6.10. 34 | * 35 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-6.10 RFC 9562, section 6.10. UUIDs That Do Not Identify the Host. 36 | */ 37 | final readonly class StaticNic implements Nic 38 | { 39 | /** 40 | * @var non-empty-string 41 | */ 42 | private string $address; 43 | 44 | /** 45 | * @param int<0, max> | non-empty-string $address A 48-bit integer or hexadecimal string. 46 | * 47 | * @throws InvalidArgument 48 | */ 49 | public function __construct(int | string $address) 50 | { 51 | if (is_int($address)) { 52 | $address = $this->parseIntegerAddress($address); 53 | } else { 54 | $address = $this->parseHexadecimalAddress($address); 55 | } 56 | 57 | $this->address = $address; 58 | } 59 | 60 | public function address(): string 61 | { 62 | return $this->address; 63 | } 64 | 65 | /** 66 | * @return non-empty-string 67 | */ 68 | private function parseIntegerAddress(int $address): string 69 | { 70 | /** @var non-empty-string */ 71 | return sprintf('%012s', dechex($address | 0x010000000000)); 72 | } 73 | 74 | /** 75 | * @return non-empty-string 76 | * 77 | * @throws InvalidArgument 78 | */ 79 | private function parseHexadecimalAddress(string $address): string 80 | { 81 | if (strspn($address, Mask::HEX) !== strlen($address) || strlen($address) > 12) { 82 | throw new InvalidArgument('Address must be a 48-bit integer or hexadecimal string'); 83 | } 84 | 85 | /** @var int[] $parts */ 86 | $parts = unpack('n3', (string) hex2bin(sprintf('%012s', $address))); 87 | 88 | /** @var non-empty-string */ 89 | return bin2hex(pack('n3', $parts[1] | 0x0100, $parts[2], $parts[3])); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Service/Nic/SystemNic.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Nic; 18 | 19 | use Psr\SimpleCache\CacheException; 20 | use Psr\SimpleCache\CacheInterface; 21 | use Ramsey\Identifier\Exception\MacAddressNotFound; 22 | use Ramsey\Identifier\Service\Os\Os; 23 | use Ramsey\Identifier\Service\Os\PhpOs; 24 | 25 | use function preg_match; 26 | use function preg_match_all; 27 | use function str_replace; 28 | use function trim; 29 | 30 | use const PREG_PATTERN_ORDER; 31 | 32 | /** 33 | * A NIC that attempts to retrieve a MAC address from the system. 34 | */ 35 | final class SystemNic implements Nic 36 | { 37 | /** 38 | * Pattern to match addresses in ifconfig and ipconfig output. 39 | */ 40 | private const IFCONFIG_PATTERN = '/[^:]([0-9a-f]{2}([:-])[0-9a-f]{2}(\2[0-9a-f]{2}){4})[^:]/i'; 41 | 42 | /** 43 | * Pattern to match addresses in sysfs stream output. 44 | */ 45 | private const SYSFS_PATTERN = '/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/i'; 46 | 47 | /** 48 | * The cache key is generated from the Adler-32 checksum of this class name. 49 | * 50 | * ``` 51 | * hash('adler32', SystemNic::class); 52 | * ``` 53 | */ 54 | private const CACHE_KEY = '__ramsey_id_34f20f6f'; 55 | 56 | /** 57 | * The system address, stored statically for better performance. 58 | * 59 | * @var non-empty-string | null 60 | */ 61 | private static ?string $address = null; 62 | 63 | /** 64 | * @param CacheInterface | null $cache An optional PSR-16 cache instance to cache the system address for faster 65 | * lookups. Be aware that use of a centralized cache might have unintended consequences if you wish to use 66 | * machine-specific addresses. If you wish for machine-specific addresses, use of a machine-local cache, such as 67 | * APCu, is preferable. 68 | */ 69 | public function __construct( 70 | private readonly ?CacheInterface $cache = null, 71 | private readonly Os $os = new PhpOs(), 72 | ) { 73 | } 74 | 75 | public function address(): string 76 | { 77 | if (self::$address === null) { 78 | try { 79 | self::$address = $this->getAddressFromCache(); 80 | } catch (CacheException $cacheException) { 81 | throw new MacAddressNotFound( 82 | message: 'Unable to retrieve MAC address from cache', 83 | previous: $cacheException, 84 | ); 85 | } 86 | } 87 | 88 | return self::$address; 89 | } 90 | 91 | /** 92 | * @return non-empty-string 93 | * 94 | * @throws CacheException 95 | */ 96 | private function getAddressFromCache(): string 97 | { 98 | /** @var string | null $address */ 99 | $address = $this->cache?->get(self::CACHE_KEY); 100 | 101 | if ($address === null || $address === '') { 102 | $address = $this->getAddressFromSystem(); 103 | $this->cache?->set(self::CACHE_KEY, $address); 104 | } 105 | 106 | return $address; 107 | } 108 | 109 | /** 110 | * Returns the system address if it can find it. 111 | * 112 | * @return non-empty-string 113 | */ 114 | private function getAddressFromSystem(): string 115 | { 116 | $address = $this->getSysfs(); 117 | 118 | if ($address === '') { 119 | $address = $this->getIfconfig(); 120 | } 121 | 122 | if ($address === '') { 123 | // If all else fails, generate a random MAC address. 124 | $address = (new RandomNic())->address(); 125 | } 126 | 127 | /** @var non-empty-string */ 128 | return str_replace([':', '-'], '', $address); 129 | } 130 | 131 | /** 132 | * Returns the MAC address from the first system interface via ifconfig, ipconfig, or netstat. 133 | */ 134 | private function getIfconfig(): string 135 | { 136 | $command = match ($this->os->getOsFamily()) { 137 | 'Windows' => 'ipconfig /all', 138 | 'Darwin' => 'ifconfig', 139 | 'BSD' => 'netstat -i -f link', 140 | default => 'netstat -ie', 141 | }; 142 | 143 | $ifconfig = $this->os->run($command); 144 | preg_match_all(self::IFCONFIG_PATTERN, $ifconfig, $matches, PREG_PATTERN_ORDER); 145 | 146 | foreach ($matches[1] as $address) { 147 | if ($address !== '00:00:00:00:00:00' && $address !== '00-00-00-00-00-00') { 148 | return $address; 149 | } 150 | } 151 | 152 | return ''; 153 | } 154 | 155 | /** 156 | * Returns the MAC address from the first system interface via the sysfs interface. 157 | */ 158 | private function getSysfs(): string 159 | { 160 | if ($this->os->getOsFamily() !== 'Linux') { 161 | return ''; 162 | } 163 | 164 | foreach ($this->os->glob('/sys/class/net/*/address') as $path) { 165 | if ($this->os->isReadable($path)) { 166 | $address = trim($this->os->fileGetContents($path)); 167 | if ($address !== '00:00:00:00:00:00' && preg_match(self::SYSFS_PATTERN, $address)) { 168 | return $address; 169 | } 170 | } 171 | } 172 | 173 | return ''; 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/Service/Os/Os.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Os; 18 | 19 | /** 20 | * An operating system. 21 | */ 22 | interface Os 23 | { 24 | /** 25 | * Returns the entire contents of the named file. 26 | * 27 | * @param string $filename The name of the file to read. 28 | */ 29 | public function fileGetContents(string $filename): string; 30 | 31 | /** 32 | * Returns the operating system family of the current system (i.e., PHP_OS_FAMILY). 33 | * 34 | * @return "Windows" | "BSD" | "Darwin" | "Solaris" | "Linux" | "Unknown" 35 | */ 36 | public function getOsFamily(): string; 37 | 38 | /** 39 | * Returns an array of paths matching the pattern. 40 | * 41 | * @link https://www.php.net/manual/en/function.glob.php PHP glob() function. 42 | * 43 | * @param string $pattern Pattern to match. 44 | * 45 | * @return string[] 46 | */ 47 | public function glob(string $pattern): array; 48 | 49 | /** 50 | * Returns true if the file or directory exists and is readable. 51 | * 52 | * @param string $filename The path to the file or directory. 53 | */ 54 | public function isReadable(string $filename): bool; 55 | 56 | /** 57 | * Executes the given command and returns its full output. 58 | * 59 | * @param string $command The command to execute. 60 | */ 61 | public function run(string $command): string; 62 | } 63 | -------------------------------------------------------------------------------- /src/Service/Os/PhpOs.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Os; 18 | 19 | use Ramsey\Identifier\Exception\MissingFunction; 20 | 21 | use function escapeshellcmd; 22 | use function file_get_contents; 23 | use function function_exists; 24 | use function glob; 25 | use function is_readable; 26 | use function shell_exec; 27 | 28 | use const GLOB_NOSORT; 29 | use const PHP_OS_FAMILY; 30 | 31 | /** 32 | * An OS that uses pure PHP functions to interact with the operating system 33 | */ 34 | class PhpOs implements Os 35 | { 36 | /** 37 | * @throws MissingFunction if a required PHP function is not available on this system 38 | */ 39 | public function __construct() 40 | { 41 | if (!function_exists('shell_exec')) { 42 | throw new MissingFunction('shell_exec() is not available on this system'); // @codeCoverageIgnore 43 | } 44 | 45 | if (!function_exists('file_get_contents')) { 46 | throw new MissingFunction('file_get_contents() is not available on this system'); // @codeCoverageIgnore 47 | } 48 | 49 | if (!function_exists('glob')) { 50 | throw new MissingFunction('glob() is not available on this system'); // @codeCoverageIgnore 51 | } 52 | 53 | if (!function_exists('is_readable')) { 54 | throw new MissingFunction('is_readable() is not available on this system'); // @codeCoverageIgnore 55 | } 56 | } 57 | 58 | public function fileGetContents(string $filename): string 59 | { 60 | return (string) file_get_contents($filename); 61 | } 62 | 63 | public function getOsFamily(): string 64 | { 65 | /** @var "Windows" | "BSD" | "Darwin" | "Solaris" | "Linux" | "Unknown" */ 66 | return PHP_OS_FAMILY; 67 | } 68 | 69 | /** 70 | * @inheritDoc 71 | */ 72 | public function glob(string $pattern): array 73 | { 74 | $paths = @glob($pattern, GLOB_NOSORT); 75 | 76 | return $paths ?: []; 77 | } 78 | 79 | public function isReadable(string $filename): bool 80 | { 81 | return is_readable($filename); 82 | } 83 | 84 | public function run(string $command): string 85 | { 86 | $command = escapeshellcmd($command); 87 | 88 | // Redirect stderr to stdout. 89 | $command .= ' 2>&1'; 90 | 91 | return (string) @shell_exec($command); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Service/Sequence/FrozenSequence.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Sequence; 18 | 19 | /** 20 | * A sequence that returns a pre-determined value as the sequence and never changes. 21 | */ 22 | final readonly class FrozenSequence implements Sequence 23 | { 24 | /** 25 | * @param int | string $value A pre-determined sequence value 26 | */ 27 | public function __construct(private int | string $value) 28 | { 29 | } 30 | 31 | public function current(?string $state = null): int | string 32 | { 33 | return $this->value; 34 | } 35 | 36 | public function next(?string $state = null): int | string 37 | { 38 | return $this->value; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Service/Sequence/MonotonicSequence.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Sequence; 18 | 19 | use Psr\SimpleCache\CacheInterface; 20 | use Ramsey\Identifier\Service\Cache\InMemoryCache; 21 | 22 | use function abs; 23 | use function spl_object_hash; 24 | 25 | use const PHP_INT_MAX; 26 | use const PHP_INT_MIN; 27 | 28 | /** 29 | * An integer sequence that always increases or decreases by a given step value. 30 | * 31 | * If the step value is positive, the sequence will be *monotonically increasing*. If the step value is negative, the 32 | * sequence will be *monotonically decreasing*. 33 | * 34 | * @link https://en.wikipedia.org/wiki/Monotonic_function "Monotonic function" on Wikipedia 35 | */ 36 | final class MonotonicSequence implements Sequence 37 | { 38 | /** 39 | * The cache key is generated from the Adler-32 checksum of this class name. 40 | * 41 | * ``` 42 | * hash('adler32', MonotonicSequence::class); 43 | * ``` 44 | */ 45 | private const CACHE_KEY = '__ramsey_id_259414de'; 46 | 47 | private ?string $defaultCacheKey = null; 48 | 49 | /** 50 | * @param int $start The sequence starting value; please note, the first call to `next()` will return this value + `$step`. 51 | * @param int $step How much the sequence should increase or decrease between values. 52 | * @param CacheInterface $cache A cache for storing the sequence and maintaining state. 53 | */ 54 | public function __construct( 55 | private readonly int $start = 0, 56 | private readonly int $step = 1, 57 | private readonly CacheInterface $cache = new InMemoryCache(), 58 | ) { 59 | } 60 | 61 | public function current(?string $state = null): int 62 | { 63 | /** @var int */ 64 | return $this->cache->get($this->generateCacheKey($state), $this->start); 65 | } 66 | 67 | public function next(?string $state = null): int 68 | { 69 | $previous = $this->current($state); 70 | 71 | if ($this->step > 0 && $previous === PHP_INT_MAX || (PHP_INT_MAX - $previous) < $this->step) { 72 | throw new SequenceOverflow('Unable to increase sequence beyond its maximum value'); 73 | } elseif ($this->step < 0 && $previous === PHP_INT_MIN || ($previous - PHP_INT_MIN) < abs($this->step)) { 74 | throw new SequenceOverflow('Unable to decrease sequence beyond its minimum value'); 75 | } 76 | 77 | $next = $previous + $this->step; 78 | $this->cache->set($this->generateCacheKey($state), $next); 79 | 80 | return $next; 81 | } 82 | 83 | private function generateCacheKey(?string $state): string 84 | { 85 | if ($state === null) { 86 | if ($this->defaultCacheKey === null) { 87 | $this->defaultCacheKey = self::CACHE_KEY 88 | . "|start={$this->start};step={$this->step}|" 89 | . spl_object_hash($this); 90 | } 91 | 92 | return $this->defaultCacheKey; 93 | } 94 | 95 | return self::CACHE_KEY . '|' . $state; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Service/Sequence/RandomSequence.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Sequence; 18 | 19 | use function random_int; 20 | 21 | use const PHP_INT_MAX; 22 | use const PHP_INT_MIN; 23 | 24 | /** 25 | * Uses PHP's `random_int()` function to generate a random sequence value. 26 | * 27 | * > [!CAUTION] 28 | * > Values generated using RandomSequence are not sequential or monotonic. They may be positive or negative integers. 29 | * 30 | * @link https://www.php.net/random_int random_int(). 31 | */ 32 | final class RandomSequence implements Sequence 33 | { 34 | private int $current; 35 | 36 | /** 37 | * @param int $min The minimum value allowed in this random sequence (inclusive). 38 | * @param int $max The maximum value allowed in this random sequence (inclusive). 39 | */ 40 | public function __construct( 41 | private readonly int $min = PHP_INT_MIN, 42 | private readonly int $max = PHP_INT_MAX, 43 | ) { 44 | // Initialize the random sequence. 45 | $this->current = random_int($this->min, $this->max); 46 | } 47 | 48 | public function current(?string $state = null): int 49 | { 50 | // This method should always return the previously generated "next" value without "advancing" the sequence. 51 | return $this->current; 52 | } 53 | 54 | public function next(?string $state = null): int 55 | { 56 | return $this->current = random_int($this->min, $this->max); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Service/Sequence/Sequence.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Sequence; 18 | 19 | /** 20 | * Derives a sequence value. 21 | * 22 | * Sequences may be ascending or descending, depending on the nature of the sequence. The `next()` method should always 23 | * return the next available value in the sequence, for the state provided. 24 | */ 25 | interface Sequence 26 | { 27 | /** 28 | * Returns the current sequence value for the given state. 29 | * 30 | * @param non-empty-string | null $state If provided, the state is treated as a namespace from which the sequence 31 | * value will be returned; each unique state has a difference sequence. 32 | */ 33 | public function current(?string $state = null): int | string; 34 | 35 | /** 36 | * Advances the sequence and returns the next value for the given state. 37 | * 38 | * If the sequence has reached the maximum value allowed for the given state, it may throw a SequenceOverflow 39 | * exception. 40 | * 41 | * @param non-empty-string | null $state If provided, the state is treated as a namespace within which the next 42 | * sequence value will be generated; each unique state has a different sequence. 43 | * 44 | * @throws SequenceOverflow when the sequence for a given state cannot be increased or decreased beyond its current 45 | * value. 46 | */ 47 | public function next(?string $state = null): int | string; 48 | } 49 | -------------------------------------------------------------------------------- /src/Service/Sequence/SequenceOverflow.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Service\Sequence; 18 | 19 | use OverflowException; 20 | use Ramsey\Identifier\Exception\IdentifierException; 21 | 22 | /** 23 | * Thrown when a sequence reaches its maximum value (i.e., the sequence cannot be incremented beyond its current value). 24 | */ 25 | class SequenceOverflow extends OverflowException implements IdentifierException 26 | { 27 | } 28 | -------------------------------------------------------------------------------- /src/Snowflake.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier; 18 | 19 | use Identifier\BytesIdentifier; 20 | use Identifier\DateTimeIdentifier; 21 | use Identifier\IntegerIdentifier; 22 | 23 | /** 24 | * A Snowflake identifier. 25 | * 26 | * @link https://github.com/twitter-archive/snowflake/tree/snowflake-2010 Twitter Snowflakes. 27 | * @link https://discord.com/developers/docs/reference#snowflakes Discord Snowflakes. 28 | * @link https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c Instagram Snowflakes. 29 | * @link https://github.com/mastodon/mastodon/blob/04492e7f934d07f8e89fa9c3d4fe3381f251e8a2/lib/mastodon/snowflake.rb Mastodon Snowflakes. 30 | */ 31 | interface Snowflake extends BytesIdentifier, DateTimeIdentifier, IntegerIdentifier 32 | { 33 | /** 34 | * Returns a string representation of the Snowflake encoded as hexadecimal digits. 35 | */ 36 | public function toHexadecimal(): string; 37 | } 38 | -------------------------------------------------------------------------------- /src/Snowflake/DiscordSnowflake.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake; 18 | 19 | use Ramsey\Identifier\Exception\InvalidArgument; 20 | use Ramsey\Identifier\Snowflake; 21 | use Ramsey\Identifier\Snowflake\Utility\Standard; 22 | 23 | final readonly class DiscordSnowflake implements Snowflake 24 | { 25 | use Standard; 26 | 27 | /** 28 | * Constructs a Snowflake identifier using Discord's Unix Epoch offset. 29 | * 30 | * @param int | numeric-string $snowflake A representation of the Snowflake in integer or numeric string form. 31 | * 32 | * @throws InvalidArgument 33 | */ 34 | public function __construct(int | string $snowflake) 35 | { 36 | $this->snowflake = new GenericSnowflake($snowflake, Epoch::Discord); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Snowflake/DiscordSnowflakeFactory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake; 18 | 19 | use Brick\Math\BigInteger; 20 | use DateTimeInterface; 21 | use Psr\Clock\ClockInterface as Clock; 22 | use Ramsey\Identifier\Exception\InvalidArgument; 23 | use Ramsey\Identifier\Service\Clock\ClockSequence; 24 | use Ramsey\Identifier\Service\Clock\MonotonicClockSequence; 25 | use Ramsey\Identifier\Service\Clock\Precision; 26 | use Ramsey\Identifier\Service\Clock\SystemClock; 27 | use Ramsey\Identifier\Snowflake\Utility\StandardFactory; 28 | use Ramsey\Identifier\SnowflakeFactory; 29 | 30 | use function sprintf; 31 | 32 | /** 33 | * A factory that generates Snowflakes according to Discord's rules 34 | * 35 | * @link https://discord.com/developers/docs/reference#snowflakes Discord Snowflakes 36 | */ 37 | final class DiscordSnowflakeFactory implements SnowflakeFactory 38 | { 39 | use StandardFactory; 40 | 41 | /** 42 | * For performance, we'll prepare the worker and process ID bits and store them for repeated use. 43 | */ 44 | private readonly int $workerProcessIdShifted; 45 | 46 | /** 47 | * We increase this value each time our clock sequence rolls over and add the value to the milliseconds to ensure 48 | * the values are monotonically increasing. 49 | */ 50 | private int $clockSequenceCounter = 0; 51 | 52 | /** 53 | * Constructs a factory for creating Discord Snowflakes. 54 | * 55 | * @param int<0, 31> $workerId A 5-bit worker identifier to use when creating Snowflakes. 56 | * @param int<0, 31> $processId A 5-bit process identifier to use when creating Snowflakes. 57 | * @param Clock $clock A clock used to provide a date-time instance; defaults to {@see SystemClock}. 58 | * @param ClockSequence $sequence A clock sequence value to prevent collisions; defaults to {@see MonotonicClockSequence}. 59 | */ 60 | public function __construct( 61 | private readonly int $workerId, 62 | private readonly int $processId, 63 | private readonly Clock $clock = new SystemClock(), 64 | private readonly ClockSequence $sequence = new MonotonicClockSequence(), 65 | ) { 66 | $this->workerProcessIdShifted = ($this->workerId & 0x1f) << 17 | ($this->processId & 0x1f) << 12; 67 | } 68 | 69 | /** 70 | * @throws InvalidArgument 71 | */ 72 | public function create(): DiscordSnowflake 73 | { 74 | return $this->createFromDateTime($this->clock->now()); 75 | } 76 | 77 | /** 78 | * @throws InvalidArgument 79 | */ 80 | public function createFromBytes(string $identifier): DiscordSnowflake 81 | { 82 | return new DiscordSnowflake($this->convertFromBytes($identifier)); 83 | } 84 | 85 | /** 86 | * @throws InvalidArgument 87 | */ 88 | public function createFromDateTime(DateTimeInterface $dateTime): DiscordSnowflake 89 | { 90 | $milliseconds = (int) $dateTime->format(Precision::Millisecond->value) - Epoch::Discord->value; 91 | 92 | if ($milliseconds < 0) { 93 | throw new InvalidArgument(sprintf( 94 | 'Timestamp may not be earlier than the Discord epoch, %s', 95 | Epoch::Discord->toIso8601(), 96 | )); 97 | } 98 | 99 | $sequence = $this->sequence->next((string) ($this->workerId + $this->processId), $dateTime) & 0x0fff; 100 | 101 | // Increase the milliseconds by the current value of the clock sequence counter. 102 | $milliseconds += $this->clockSequenceCounter; 103 | $millisecondsShifted = $milliseconds << 22; 104 | 105 | if ($sequence === 0x0fff) { 106 | // If the sequence is currently 0x0fff, bump the clock sequence counter, since we're rolling over. 107 | $this->clockSequenceCounter++; 108 | } 109 | 110 | if ($millisecondsShifted > $milliseconds) { 111 | $identifier = $millisecondsShifted | $this->workerProcessIdShifted | $sequence; 112 | } else { 113 | /** @var numeric-string $identifier */ 114 | $identifier = (string) BigInteger::of($milliseconds) 115 | ->shiftedLeft(22) 116 | ->or($this->workerProcessIdShifted) 117 | ->or($sequence); 118 | } 119 | 120 | return new DiscordSnowflake($identifier); 121 | } 122 | 123 | /** 124 | * @throws InvalidArgument 125 | */ 126 | public function createFromHexadecimal(string $identifier): DiscordSnowflake 127 | { 128 | return new DiscordSnowflake($this->convertFromHexadecimal($identifier)); 129 | } 130 | 131 | /** 132 | * @throws InvalidArgument 133 | */ 134 | public function createFromInteger(int | string $identifier): DiscordSnowflake 135 | { 136 | return new DiscordSnowflake($identifier); 137 | } 138 | 139 | /** 140 | * @throws InvalidArgument 141 | */ 142 | public function createFromString(string $identifier): DiscordSnowflake 143 | { 144 | /** @var numeric-string $value */ 145 | $value = $identifier; 146 | 147 | return new DiscordSnowflake($value); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Snowflake/Epoch.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake; 18 | 19 | use DateTimeImmutable; 20 | 21 | /** 22 | * Well-known Snowflake epochs. 23 | */ 24 | enum Epoch: int 25 | { 26 | /** 27 | * The Discord epoch begins at 2015-01-01 00:00:00.000 +00:00. 28 | * 29 | * @link https://discord.com/developers/docs/reference#snowflakes Discord Snowflakes. 30 | */ 31 | case Discord = 1_420_070_400_000; 32 | 33 | /** 34 | * The Instagram epoch begins at 2011-08-24 21:07:01.721 +00:00. 35 | * 36 | * The calculation within the Instagram blog post appears incorrect. The blog post states: 37 | * 38 | * > Let's walk through an example: let's say it's September 9th, 2011, at 5:00pm and our 'epoch' begins on January 39 | * > 1st, 2011. There have been 1387263000 milliseconds since the beginning of our epoch… 40 | * 41 | * If the current date is September 9th, 2011, at 5:00pm, and there have been 1,387,263,000 milliseconds since the 42 | * beginning of the epoch, then the epoch must have begun on August 24, 2011. Later in the same post, they use some 43 | * PL/PGSQL code to illustrate the calculation. In it, the epoch is written as `1314220021721`, which is on August 44 | * 24, 2011, if this is a count of milliseconds since the Unix epoch. This is the value we use, since it appears to 45 | * align with all their examples. 46 | * 47 | * @link https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c Instagram Snowflakes. 48 | */ 49 | case Instagram = 1_314_220_021_721; 50 | 51 | /** 52 | * The Twitter epoch begins at 2010-11-04 01:42:54.657 +00:00. 53 | * 54 | * @link https://github.com/twitter-archive/snowflake/blob/snowflake-2010/src/main/scala/com/twitter/service/snowflake/IdWorker.scala#L25 Twitter Snowflakes. 55 | */ 56 | case Twitter = 1_288_834_974_657; 57 | 58 | /** 59 | * ISO 8601 extended format (includes millisecond precision). 60 | */ 61 | public const ISO_EXTENDED_FORMAT = 'Y-m-d\TH:i:s.vp'; 62 | 63 | /** 64 | * Returns the epoch as a date-time string in ISO 8601 extended format. 65 | */ 66 | public function toIso8601(): string 67 | { 68 | return (new DateTimeImmutable('@' . $this->value / 1000))->format(self::ISO_EXTENDED_FORMAT); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Snowflake/GenericSnowflake.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake; 18 | 19 | use DateTimeImmutable; 20 | use Identifier\BytesIdentifier; 21 | use Identifier\Exception\OutOfRange; 22 | use Ramsey\Identifier\Exception\InvalidArgument; 23 | use Ramsey\Identifier\Exception\NotComparable; 24 | use Ramsey\Identifier\Snowflake; 25 | use Ramsey\Identifier\Snowflake\Utility\Format; 26 | use Ramsey\Identifier\Snowflake\Utility\Mask; 27 | use Ramsey\Identifier\Snowflake\Utility\Time; 28 | use Ramsey\Identifier\Snowflake\Utility\Validation; 29 | use Stringable; 30 | 31 | use function assert; 32 | use function gettype; 33 | use function is_int; 34 | use function is_scalar; 35 | use function sprintf; 36 | use function strlen; 37 | use function strspn; 38 | 39 | final readonly class GenericSnowflake implements Snowflake 40 | { 41 | use Validation; 42 | 43 | private Time $time; 44 | 45 | private int | string $epochOffset; 46 | 47 | /** 48 | * Constructs a {@see Snowflake} instance 49 | * 50 | * @param int | numeric-string $snowflake A representation of the Snowflake in integer or numeric string form 51 | * @param Epoch | int | numeric-string $epochOffset The Snowflake ID's offset from the Unix Epoch in milliseconds 52 | * 53 | * @throws InvalidArgument 54 | */ 55 | public function __construct( 56 | private int | string $snowflake, 57 | Epoch | int | string $epochOffset, 58 | ) { 59 | if (!$this->isValid($this->snowflake)) { 60 | throw new InvalidArgument(sprintf('Invalid Snowflake: "%s"', $this->snowflake)); 61 | } 62 | 63 | if ($epochOffset instanceof Epoch) { 64 | $epochOffset = $epochOffset->value; 65 | } 66 | 67 | if (!is_int($epochOffset) && strspn($epochOffset, Mask::INT) !== strlen($epochOffset)) { 68 | throw new InvalidArgument(sprintf('Invalid epoch offset: "%s"', $epochOffset)); 69 | } 70 | 71 | $this->epochOffset = $epochOffset; 72 | $this->time = new Time(); 73 | } 74 | 75 | /** 76 | * @return array{snowflake: int | numeric-string, epochOffset: int | numeric-string} 77 | */ 78 | public function __serialize(): array 79 | { 80 | return ['snowflake' => $this->snowflake, 'epochOffset' => $this->epochOffset]; 81 | } 82 | 83 | /** 84 | * @return non-empty-string 85 | */ 86 | public function __toString(): string 87 | { 88 | return (string) $this->snowflake; 89 | } 90 | 91 | /** 92 | * @param array{snowflake: int | numeric-string, epochOffset: int | numeric-string} $data 93 | * 94 | * @throws InvalidArgument 95 | */ 96 | public function __unserialize(array $data): void 97 | { 98 | assert(isset($data['snowflake']), "'snowflake' is not set in serialized data"); 99 | assert(isset($data['epochOffset']), "'epochOffset' is not set in serialized data"); 100 | 101 | $this->__construct($data['snowflake'], $data['epochOffset']); 102 | } 103 | 104 | /** 105 | * @throws NotComparable 106 | */ 107 | public function compareTo(mixed $other): int 108 | { 109 | if ($other instanceof BytesIdentifier) { 110 | return $this->toBytes() <=> $other->toBytes(); 111 | } 112 | 113 | if ($other === null || is_scalar($other) || $other instanceof Stringable) { 114 | return (string) $this->snowflake <=> (string) $other; 115 | } 116 | 117 | throw new NotComparable(sprintf( 118 | 'Comparison with values of type "%s" is not supported', 119 | gettype($other), 120 | )); 121 | } 122 | 123 | public function equals(mixed $other): bool 124 | { 125 | try { 126 | return $this->compareTo($other) === 0; 127 | } catch (NotComparable) { 128 | return false; 129 | } 130 | } 131 | 132 | /** 133 | * @throws OutOfRange 134 | */ 135 | public function getDateTime(): DateTimeImmutable 136 | { 137 | return $this->time->getDateTimeForSnowflake($this, $this->epochOffset, 22); 138 | } 139 | 140 | /** 141 | * @return non-empty-string 142 | */ 143 | public function jsonSerialize(): string 144 | { 145 | return (string) $this->snowflake; 146 | } 147 | 148 | /** 149 | * @return non-empty-string 150 | */ 151 | public function toBytes(): string 152 | { 153 | return Format::formatBytes($this->snowflake); 154 | } 155 | 156 | /** 157 | * @return non-empty-string 158 | */ 159 | public function toHexadecimal(): string 160 | { 161 | return Format::formatHex($this->snowflake); 162 | } 163 | 164 | /** 165 | * @return int<0, max> | numeric-string 166 | */ 167 | public function toInteger(): int | string 168 | { 169 | return Format::formatInt($this->snowflake); 170 | } 171 | 172 | /** 173 | * @return non-empty-string 174 | */ 175 | public function toString(): string 176 | { 177 | return (string) $this->snowflake; 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Snowflake/InstagramSnowflake.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake; 18 | 19 | use DateTimeImmutable; 20 | use Identifier\Exception\OutOfRange; 21 | use Ramsey\Identifier\Exception\InvalidArgument; 22 | use Ramsey\Identifier\Snowflake; 23 | use Ramsey\Identifier\Snowflake\Utility\Standard; 24 | use Ramsey\Identifier\Snowflake\Utility\Time; 25 | 26 | final readonly class InstagramSnowflake implements Snowflake 27 | { 28 | use Standard; 29 | 30 | private Time $time; 31 | 32 | /** 33 | * Constructs a Snowflake identifier using Instagram's Unix Epoch offset. 34 | * 35 | * @param int | numeric-string $snowflake A representation of the Snowflake in integer or numeric string form. 36 | * 37 | * @throws InvalidArgument 38 | */ 39 | public function __construct(int | string $snowflake) 40 | { 41 | $this->snowflake = new GenericSnowflake($snowflake, Epoch::Instagram); 42 | $this->time = new Time(); 43 | } 44 | 45 | /** 46 | * @throws OutOfRange 47 | */ 48 | public function getDateTime(): DateTimeImmutable 49 | { 50 | return $this->time->getDateTimeForSnowflake($this, Epoch::Instagram->value, 23); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Snowflake/InstagramSnowflakeFactory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake; 18 | 19 | use Brick\Math\BigInteger; 20 | use DateTimeInterface; 21 | use Psr\Clock\ClockInterface as Clock; 22 | use Ramsey\Identifier\Exception\InvalidArgument; 23 | use Ramsey\Identifier\Service\Clock\ClockSequence; 24 | use Ramsey\Identifier\Service\Clock\MonotonicClockSequence; 25 | use Ramsey\Identifier\Service\Clock\Precision; 26 | use Ramsey\Identifier\Service\Clock\SystemClock; 27 | use Ramsey\Identifier\Snowflake\Utility\StandardFactory; 28 | use Ramsey\Identifier\SnowflakeFactory; 29 | 30 | use function sprintf; 31 | 32 | /** 33 | * A factory that generates Snowflakes according to Instagram's rules. 34 | * 35 | * @link https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c Instagram Snowflakes. 36 | */ 37 | final class InstagramSnowflakeFactory implements SnowflakeFactory 38 | { 39 | use StandardFactory; 40 | 41 | /** 42 | * We increase this value each time our clock sequence rolls over and add the value to the milliseconds to ensure 43 | * the values are monotonically increasing. 44 | */ 45 | private int $clockSequenceCounter = 0; 46 | 47 | /** 48 | * For performance, we'll prepare the shared ID bits and store them for repeated use. 49 | */ 50 | private readonly int $shardIdShifted; 51 | 52 | /** 53 | * Constructs a factory for creating Instagram Snowflakes. 54 | * 55 | * @param int<0, 8191> $shardId A 13-bit shard identifier to use when creating Snowflakes. 56 | * @param Clock $clock A clock used to provide a date-time instance; defaults to {@see SystemClock}. 57 | * @param ClockSequence $sequence A clock sequence value to prevent collisions; defaults to {@see MonotonicClockSequence}. 58 | */ 59 | public function __construct( 60 | private readonly int $shardId, 61 | private readonly Clock $clock = new SystemClock(), 62 | private readonly ClockSequence $sequence = new MonotonicClockSequence(), 63 | ) { 64 | $this->shardIdShifted = ($this->shardId & 0x1fff) << 10; 65 | } 66 | 67 | /** 68 | * @throws InvalidArgument 69 | */ 70 | public function create(): InstagramSnowflake 71 | { 72 | return $this->createFromDateTime($this->clock->now()); 73 | } 74 | 75 | /** 76 | * @throws InvalidArgument 77 | */ 78 | public function createFromBytes(string $identifier): InstagramSnowflake 79 | { 80 | return new InstagramSnowflake($this->convertFromBytes($identifier)); 81 | } 82 | 83 | /** 84 | * @throws InvalidArgument 85 | */ 86 | public function createFromDateTime(DateTimeInterface $dateTime): InstagramSnowflake 87 | { 88 | $milliseconds = (int) $dateTime->format(Precision::Millisecond->value) - Epoch::Instagram->value; 89 | 90 | if ($milliseconds < 0) { 91 | throw new InvalidArgument(sprintf( 92 | 'Timestamp may not be earlier than the Instagram epoch, %s', 93 | Epoch::Instagram->toIso8601(), 94 | )); 95 | } 96 | 97 | $sequence = $this->sequence->next((string) $this->shardId, $dateTime) & 0x03ff; 98 | 99 | // Increase the milliseconds by the current value of the clock sequence counter. 100 | $milliseconds += $this->clockSequenceCounter; 101 | $millisecondsShifted = $milliseconds << 23; 102 | 103 | if ($sequence === 0x03ff) { 104 | // If the sequence is currently 0x03ff, bump the clock sequence counter, since we're rolling over. 105 | $this->clockSequenceCounter++; 106 | } 107 | 108 | if ($millisecondsShifted > $milliseconds) { 109 | $identifier = $millisecondsShifted | $this->shardIdShifted | $sequence; 110 | } else { 111 | /** @var numeric-string $identifier */ 112 | $identifier = (string) BigInteger::of($milliseconds) 113 | ->shiftedLeft(23) 114 | ->or($this->shardIdShifted) 115 | ->or($sequence); 116 | } 117 | 118 | return new InstagramSnowflake($identifier); 119 | } 120 | 121 | /** 122 | * @throws InvalidArgument 123 | */ 124 | public function createFromHexadecimal(string $identifier): InstagramSnowflake 125 | { 126 | return new InstagramSnowflake($this->convertFromHexadecimal($identifier)); 127 | } 128 | 129 | /** 130 | * @throws InvalidArgument 131 | */ 132 | public function createFromInteger(int | string $identifier): InstagramSnowflake 133 | { 134 | return new InstagramSnowflake($identifier); 135 | } 136 | 137 | /** 138 | * @throws InvalidArgument 139 | */ 140 | public function createFromString(string $identifier): InstagramSnowflake 141 | { 142 | /** @var numeric-string $value */ 143 | $value = $identifier; 144 | 145 | return new InstagramSnowflake($value); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/Snowflake/TwitterSnowflake.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake; 18 | 19 | use Ramsey\Identifier\Exception\InvalidArgument; 20 | use Ramsey\Identifier\Snowflake; 21 | use Ramsey\Identifier\Snowflake\Utility\Standard; 22 | 23 | final readonly class TwitterSnowflake implements Snowflake 24 | { 25 | use Standard; 26 | 27 | /** 28 | * Constructs a Snowflake identifier using Twitter's Unix Epoch offset. 29 | * 30 | * @param int | numeric-string $snowflake A representation of the Snowflake in integer or numeric string form. 31 | * 32 | * @throws InvalidArgument 33 | */ 34 | public function __construct(int | string $snowflake) 35 | { 36 | $this->snowflake = new GenericSnowflake($snowflake, Epoch::Twitter); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Snowflake/TwitterSnowflakeFactory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake; 18 | 19 | use Brick\Math\BigInteger; 20 | use DateTimeInterface; 21 | use Psr\Clock\ClockInterface as Clock; 22 | use Ramsey\Identifier\Exception\InvalidArgument; 23 | use Ramsey\Identifier\Service\Clock\ClockSequence; 24 | use Ramsey\Identifier\Service\Clock\MonotonicClockSequence; 25 | use Ramsey\Identifier\Service\Clock\SystemClock; 26 | use Ramsey\Identifier\Snowflake\Utility\StandardFactory; 27 | use Ramsey\Identifier\SnowflakeFactory; 28 | 29 | use function sprintf; 30 | 31 | /** 32 | * A factory that generates Snowflakes according to Twitter's rules. 33 | * 34 | * @link https://github.com/twitter-archive/snowflake/tree/snowflake-2010 Twitter Snowflakes. 35 | */ 36 | final class TwitterSnowflakeFactory implements SnowflakeFactory 37 | { 38 | use StandardFactory; 39 | 40 | /** 41 | * For performance, we'll prepare the machine ID bits and store them for repeated use. 42 | */ 43 | private readonly int $machineIdShifted; 44 | 45 | /** 46 | * We increase this value each time our clock sequence rolls over and add the value to the milliseconds to ensure 47 | * the values are monotonically increasing. 48 | */ 49 | private int $clockSequenceCounter = 0; 50 | 51 | /** 52 | * Constructs a factory for creating Twitter Snowflakes. 53 | * 54 | * @param int<0, 1023> $machineId A 10-bit machine identifier to use when creating Snowflakes. 55 | * @param Clock $clock A clock used to provide a date-time instance; defaults to {@see SystemClock}. 56 | * @param ClockSequence $sequence A clock sequence value to prevent collisions; defaults to {@see MonotonicClockSequence}. 57 | */ 58 | public function __construct( 59 | private readonly int $machineId, 60 | private readonly Clock $clock = new SystemClock(), 61 | private readonly ClockSequence $sequence = new MonotonicClockSequence(), 62 | ) { 63 | $this->machineIdShifted = ($this->machineId & 0x03ff) << 12; 64 | } 65 | 66 | /** 67 | * @throws InvalidArgument 68 | */ 69 | public function create(): TwitterSnowflake 70 | { 71 | return $this->createFromDateTime($this->clock->now()); 72 | } 73 | 74 | /** 75 | * @throws InvalidArgument 76 | */ 77 | public function createFromBytes(string $identifier): TwitterSnowflake 78 | { 79 | return new TwitterSnowflake($this->convertFromBytes($identifier)); 80 | } 81 | 82 | /** 83 | * @throws InvalidArgument 84 | */ 85 | public function createFromDateTime(DateTimeInterface $dateTime): TwitterSnowflake 86 | { 87 | $milliseconds = (int) $dateTime->format('Uv') - Epoch::Twitter->value; 88 | 89 | if ($milliseconds < 0) { 90 | throw new InvalidArgument(sprintf( 91 | 'Timestamp may not be earlier than the Twitter epoch, %s', 92 | Epoch::Twitter->toIso8601(), 93 | )); 94 | } 95 | 96 | $sequence = $this->sequence->next((string) $this->machineId, $dateTime) & 0x0fff; 97 | 98 | // Increase the milliseconds by the current value of the clock sequence counter. 99 | $milliseconds += $this->clockSequenceCounter; 100 | $millisecondsShifted = $milliseconds << 22; 101 | 102 | if ($sequence === 0x0fff) { 103 | // If the sequence is currently 0x0fff, bump the clock sequence counter, since we're rolling over. 104 | $this->clockSequenceCounter++; 105 | } 106 | 107 | if ($millisecondsShifted > $milliseconds) { 108 | $identifier = $millisecondsShifted | $this->machineIdShifted | $sequence; 109 | } else { 110 | /** @var numeric-string $identifier */ 111 | $identifier = (string) BigInteger::of($milliseconds) 112 | ->shiftedLeft(22) 113 | ->or($this->machineIdShifted) 114 | ->or($sequence); 115 | } 116 | 117 | return new TwitterSnowflake($identifier); 118 | } 119 | 120 | /** 121 | * @throws InvalidArgument 122 | */ 123 | public function createFromHexadecimal(string $identifier): TwitterSnowflake 124 | { 125 | return new TwitterSnowflake($this->convertFromHexadecimal($identifier)); 126 | } 127 | 128 | /** 129 | * @throws InvalidArgument 130 | */ 131 | public function createFromInteger(int | string $identifier): TwitterSnowflake 132 | { 133 | return new TwitterSnowflake($identifier); 134 | } 135 | 136 | /** 137 | * @throws InvalidArgument 138 | */ 139 | public function createFromString(string $identifier): TwitterSnowflake 140 | { 141 | /** @var numeric-string $value */ 142 | $value = $identifier; 143 | 144 | return new TwitterSnowflake($value); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Snowflake/Utility/Format.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake\Utility; 18 | 19 | use Brick\Math\BigInteger; 20 | 21 | use function is_int; 22 | use function is_string; 23 | use function pack; 24 | use function sprintf; 25 | use function str_pad; 26 | use function strlen; 27 | use function strspn; 28 | 29 | use const STR_PAD_LEFT; 30 | 31 | /** 32 | * @internal 33 | */ 34 | enum Format: int 35 | { 36 | /** 37 | * Bytes representation. 38 | */ 39 | case Bytes = 8; 40 | 41 | /** 42 | * Hexadecimal representation. 43 | */ 44 | case Hex = 16; 45 | 46 | /** 47 | * Formats a Snowflake identifier from its integer or numeric string form into {@see self::Hex}, {@see self::Bytes}, 48 | * or int forms. 49 | */ 50 | public static function format(int | string $value, ?self $to): int | string 51 | { 52 | if (is_string($value) && self::isStringInt($value) && is_int($value + 0)) { 53 | $value = (int) $value; 54 | } 55 | 56 | return match ($to) { 57 | self::Hex => is_int($value) 58 | ? sprintf('%016x', $value) 59 | : sprintf('%016s', BigInteger::of($value)->toBase(16)), 60 | self::Bytes => is_int($value) 61 | ? pack('J', $value) 62 | : str_pad(BigInteger::of($value)->toBytes(false), 8, "\x00", STR_PAD_LEFT), 63 | default => $value, 64 | }; 65 | } 66 | 67 | /** 68 | * @return non-empty-string 69 | */ 70 | public static function formatBytes(int | string $value): string 71 | { 72 | /** @var non-empty-string */ 73 | return self::format($value, self::Bytes); 74 | } 75 | 76 | /** 77 | * @return non-empty-string 78 | */ 79 | public static function formatHex(int | string $value): string 80 | { 81 | /** @var non-empty-string */ 82 | return self::format($value, self::Hex); 83 | } 84 | 85 | /** 86 | * @return int<0, max> | numeric-string 87 | */ 88 | public static function formatInt(int | string $value): int | string 89 | { 90 | /** @var int<0, max> | numeric-string */ 91 | return self::format($value, null); 92 | } 93 | 94 | /** 95 | * @phpstan-assert-if-true numeric-string $value 96 | */ 97 | private static function isStringInt(string $value): bool 98 | { 99 | return strspn($value, Mask::INT) === strlen($value); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Snowflake/Utility/Mask.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake\Utility; 18 | 19 | /** 20 | * @internal 21 | */ 22 | final class Mask 23 | { 24 | /** 25 | * A mask used with functions like {@see strspn()} to validate hexadecimal strings 26 | */ 27 | public const HEX = '0123456789abcdefABCDEF'; 28 | 29 | /** 30 | * A mask used with functions like {@see strspn()} to validate string integers 31 | */ 32 | public const INT = '0123456789'; 33 | 34 | /** 35 | * @codeCoverageIgnore 36 | */ 37 | private function __construct() 38 | { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Snowflake/Utility/Standard.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake\Utility; 18 | 19 | use DateTimeImmutable; 20 | use Identifier\Exception\OutOfRange; 21 | use Ramsey\Identifier\Exception\InvalidArgument; 22 | use Ramsey\Identifier\Exception\NotComparable; 23 | use Ramsey\Identifier\Snowflake; 24 | 25 | use function assert; 26 | 27 | /** 28 | * @internal 29 | */ 30 | trait Standard 31 | { 32 | private readonly Snowflake $snowflake; 33 | 34 | /** 35 | * @return array{snowflake: non-empty-string} 36 | */ 37 | public function __serialize(): array 38 | { 39 | return ['snowflake' => $this->snowflake->toString()]; 40 | } 41 | 42 | /** 43 | * @return non-empty-string 44 | */ 45 | public function __toString(): string 46 | { 47 | return $this->snowflake->toString(); 48 | } 49 | 50 | /** 51 | * @param array{snowflake: int | numeric-string} $data 52 | * 53 | * @throws InvalidArgument 54 | */ 55 | public function __unserialize(array $data): void 56 | { 57 | assert(isset($data['snowflake']), "'snowflake' is not set in serialized data"); 58 | 59 | $this->__construct($data['snowflake']); 60 | } 61 | 62 | /** 63 | * @throws NotComparable 64 | */ 65 | public function compareTo(mixed $other): int 66 | { 67 | return $this->snowflake->compareTo($other); 68 | } 69 | 70 | public function equals(mixed $other): bool 71 | { 72 | return $this->snowflake->equals($other); 73 | } 74 | 75 | public function getDateTime(): DateTimeImmutable 76 | { 77 | return $this->snowflake->getDateTime(); 78 | } 79 | 80 | /* 81 | * @return non-empty-string 82 | */ 83 | public function jsonSerialize(): string 84 | { 85 | return $this->snowflake->toString(); 86 | } 87 | 88 | /** 89 | * @return non-empty-string 90 | */ 91 | public function toBytes(): string 92 | { 93 | /** @var non-empty-string */ 94 | return $this->snowflake->toBytes(); 95 | } 96 | 97 | /** 98 | * @return non-empty-string 99 | */ 100 | public function toHexadecimal(): string 101 | { 102 | /** @var non-empty-string */ 103 | return $this->snowflake->toHexadecimal(); 104 | } 105 | 106 | /** 107 | * @return int<0, max> | numeric-string 108 | * 109 | * @throws OutOfRange 110 | */ 111 | public function toInteger(): int | string 112 | { 113 | /** @var int<0, max> | numeric-string */ 114 | return $this->snowflake->toInteger(); 115 | } 116 | 117 | /** 118 | * @return non-empty-string 119 | */ 120 | public function toString(): string 121 | { 122 | return $this->snowflake->toString(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Snowflake/Utility/StandardFactory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake\Utility; 18 | 19 | use Brick\Math\BigInteger; 20 | use Ramsey\Identifier\Exception\InvalidArgument; 21 | 22 | use function hexdec; 23 | use function strlen; 24 | use function strspn; 25 | use function unpack; 26 | 27 | /** 28 | * This internal trait provides common factory functionality for Snowflakes 29 | * 30 | * @internal 31 | */ 32 | trait StandardFactory 33 | { 34 | /** 35 | * @return int<0, max> | numeric-string 36 | * 37 | * @throws InvalidArgument 38 | */ 39 | private function convertFromBytes(string $identifier): int | string 40 | { 41 | if (strlen($identifier) !== Format::Bytes->value) { 42 | throw new InvalidArgument('Identifier must be an 8-byte string'); 43 | } 44 | 45 | /** @var int[] $parts */ 46 | $parts = unpack('J', $identifier); 47 | 48 | // Support unsigned 64-bit identifiers. 49 | if ($parts[1] < 0) { 50 | /** @var numeric-string */ 51 | return (string) BigInteger::fromBytes($identifier, false); 52 | } 53 | 54 | /** @var int<0, max> */ 55 | return $parts[1]; 56 | } 57 | 58 | /** 59 | * @return int<0, max> | numeric-string 60 | * 61 | * @throws InvalidArgument 62 | */ 63 | private function convertFromHexadecimal(string $identifier): int | string 64 | { 65 | if (strlen($identifier) !== Format::Hex->value || strspn($identifier, Mask::HEX) !== strlen($identifier)) { 66 | throw new InvalidArgument('Identifier must be a 16-character hexadecimal string'); 67 | } 68 | 69 | // Support unsigned 64-bit identifiers. 70 | if ($identifier > '7fffffffffffffff') { 71 | /** @var numeric-string */ 72 | return (string) BigInteger::fromBase($identifier, 16); 73 | } 74 | 75 | /** @var int<0, max> */ 76 | return hexdec($identifier); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Snowflake/Utility/Time.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake\Utility; 18 | 19 | use Brick\Math\BigInteger; 20 | use Brick\Math\RoundingMode; 21 | use DateTimeImmutable; 22 | use Identifier\Exception\OutOfRange; 23 | use Ramsey\Identifier\Snowflake; 24 | 25 | use function abs; 26 | use function intdiv; 27 | use function is_int; 28 | use function sprintf; 29 | 30 | /** 31 | * @internal 32 | */ 33 | final class Time 34 | { 35 | /** 36 | * Returns a date-time instance created from the timestamp extracted from a Snowflake. 37 | * 38 | * @param int | numeric-string $epochOffset The number of milliseconds from the Unix Epoch to offset the starting 39 | * epoch for this Snowflake. 40 | * 41 | * @throws OutOfRange 42 | */ 43 | public function getDateTimeForSnowflake( 44 | Snowflake $snowflake, 45 | int | string $epochOffset, 46 | int $rightShifts, 47 | ): DateTimeImmutable { 48 | $value = $snowflake->toInteger(); 49 | 50 | // We support unsigned, 64-bit integers, so $value might be greater than PHP_INT_MAX, in which case, it'll be a 51 | // string, and we'll need to use BigInteger for the math. 52 | if (is_int($value)) { 53 | $milliseconds = (int) (($value >> $rightShifts) + $epochOffset); 54 | $timestamp = sprintf('%d.%03d', intdiv($milliseconds, 1000), abs($milliseconds) % 1000); 55 | } else { 56 | $timestamp = (string) BigInteger::of($value) 57 | ->shiftedRight($rightShifts) 58 | ->plus($epochOffset) 59 | ->toBigDecimal() 60 | ->dividedBy(1000, 3, RoundingMode::HALF_UP); 61 | } 62 | 63 | return new DateTimeImmutable('@' . $timestamp); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Snowflake/Utility/Validation.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Snowflake\Utility; 18 | 19 | use function is_int; 20 | use function strlen; 21 | use function strspn; 22 | 23 | /** 24 | * This internal trait provides common validation functionality for Snowflakes 25 | * 26 | * @internal 27 | */ 28 | trait Validation 29 | { 30 | /** 31 | * The maximum possible value of a Snowflake (i.e., 0xffffffffffffffff) 32 | */ 33 | private const UPPER_BOUNDS = '18446744073709551615'; 34 | 35 | /** 36 | * Returns true if the Snowflake is valid, according to the given format 37 | */ 38 | private function isValid(int | string $snowflake): bool 39 | { 40 | if (is_int($snowflake)) { 41 | return $snowflake >= 0; 42 | } 43 | 44 | if ($snowflake === '') { 45 | return false; 46 | } 47 | 48 | if (strspn($snowflake, Mask::INT) !== strlen($snowflake)) { 49 | return false; 50 | } 51 | 52 | return $snowflake <= self::UPPER_BOUNDS; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/SnowflakeFactory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier; 18 | 19 | use DateTimeInterface; 20 | use Identifier\BytesIdentifierFactory; 21 | use Identifier\DateTimeIdentifierFactory; 22 | use Identifier\IntegerIdentifierFactory; 23 | use Identifier\StringIdentifierFactory; 24 | use Ramsey\Identifier\Exception\InvalidArgument; 25 | 26 | /** 27 | * A factory for creating Snowflake IDs. 28 | */ 29 | interface SnowflakeFactory extends 30 | BytesIdentifierFactory, 31 | DateTimeIdentifierFactory, 32 | IntegerIdentifierFactory, 33 | StringIdentifierFactory 34 | { 35 | public function create(): Snowflake; 36 | 37 | public function createFromBytes(string $identifier): Snowflake; 38 | 39 | public function createFromDateTime(DateTimeInterface $dateTime): Snowflake; 40 | 41 | /** 42 | * Creates a new instance of a Snowflake ID from the given hexadecimal representation. 43 | * 44 | * @throws InvalidArgument MUST throw if the identifier is not a legal value. 45 | */ 46 | public function createFromHexadecimal(string $identifier): Snowflake; 47 | 48 | public function createFromInteger(int | string $identifier): Snowflake; 49 | 50 | public function createFromString(string $identifier): Snowflake; 51 | } 52 | -------------------------------------------------------------------------------- /src/TimeBasedUuid.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier; 18 | 19 | use Identifier\DateTimeIdentifier; 20 | 21 | /** 22 | * A time-based UUID. 23 | */ 24 | interface TimeBasedUuid extends DateTimeIdentifier, Uuid 25 | { 26 | } 27 | -------------------------------------------------------------------------------- /src/TimeBasedUuidFactory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier; 18 | 19 | use DateTimeInterface; 20 | use Identifier\DateTimeIdentifierFactory; 21 | 22 | /** 23 | * A factory for creating time-based UUIDs. 24 | */ 25 | interface TimeBasedUuidFactory extends DateTimeIdentifierFactory, UuidFactory 26 | { 27 | public function create(): TimeBasedUuid; 28 | 29 | public function createFromBytes(string $identifier): TimeBasedUuid; 30 | 31 | public function createFromDateTime(DateTimeInterface $dateTime): TimeBasedUuid; 32 | 33 | public function createFromHexadecimal(string $identifier): TimeBasedUuid; 34 | 35 | public function createFromInteger(int | string $identifier): TimeBasedUuid; 36 | 37 | public function createFromString(string $identifier): TimeBasedUuid; 38 | } 39 | -------------------------------------------------------------------------------- /src/Ulid.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier; 18 | 19 | use Identifier\BytesIdentifier; 20 | use Identifier\DateTimeIdentifier; 21 | use Identifier\IntegerIdentifier; 22 | 23 | /** 24 | * A universally unique lexicographically sortable identifier (ULID). 25 | * 26 | * @link https://github.com/ulid/spec ULID Specification. 27 | */ 28 | interface Ulid extends BytesIdentifier, DateTimeIdentifier, IntegerIdentifier 29 | { 30 | /** 31 | * Returns a string representation of the ULID encoded as hexadecimal digits. 32 | */ 33 | public function toHexadecimal(): string; 34 | } 35 | -------------------------------------------------------------------------------- /src/Ulid/MaxUlid.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Ulid; 18 | 19 | use Ramsey\Identifier\Exception\InvalidArgument; 20 | use Ramsey\Identifier\Ulid as UlidInterface; 21 | use Ramsey\Identifier\Ulid\Utility\Format; 22 | use Ramsey\Identifier\Ulid\Utility\Standard; 23 | 24 | use function sprintf; 25 | use function strlen; 26 | 27 | final readonly class MaxUlid implements UlidInterface 28 | { 29 | use Standard; 30 | 31 | private const MAX = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"; 32 | 33 | /** 34 | * @throws InvalidArgument 35 | */ 36 | public function __construct(private string $ulid = self::MAX) 37 | { 38 | $this->format = Format::tryFrom(strlen($ulid)); 39 | 40 | if (!$this->isValid($this->ulid, $this->format)) { 41 | throw new InvalidArgument(sprintf('Invalid Max ULID: "%s"', $this->ulid)); 42 | } 43 | } 44 | 45 | private function isValid(string $ulid, ?Format $format): bool 46 | { 47 | return $this->isMax($ulid, $format); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Ulid/NilUlid.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Ulid; 18 | 19 | use Ramsey\Identifier\Exception\InvalidArgument; 20 | use Ramsey\Identifier\Ulid as UlidInterface; 21 | use Ramsey\Identifier\Ulid\Utility\Format; 22 | use Ramsey\Identifier\Ulid\Utility\Standard; 23 | 24 | use function sprintf; 25 | use function strlen; 26 | 27 | final readonly class NilUlid implements UlidInterface 28 | { 29 | use Standard; 30 | 31 | private const NIL = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; 32 | 33 | /** 34 | * @throws InvalidArgument 35 | */ 36 | public function __construct(private string $ulid = self::NIL) 37 | { 38 | $this->format = Format::tryFrom(strlen($ulid)); 39 | 40 | if (!$this->isValid($this->ulid, $this->format)) { 41 | throw new InvalidArgument(sprintf('Invalid Nil ULID: "%s"', $this->ulid)); 42 | } 43 | } 44 | 45 | private function isValid(string $ulid, ?Format $format): bool 46 | { 47 | return $this->isNil($ulid, $format); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Ulid/Ulid.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Ulid; 18 | 19 | use Ramsey\Identifier\Ulid as UlidInterface; 20 | use Ramsey\Identifier\Ulid\Utility\Standard; 21 | 22 | final readonly class Ulid implements UlidInterface 23 | { 24 | use Standard; 25 | } 26 | -------------------------------------------------------------------------------- /src/Ulid/Utility/Format.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Ulid\Utility; 18 | 19 | /** 20 | * @internal 21 | */ 22 | enum Format: int 23 | { 24 | /** 25 | * Bytes representation 26 | */ 27 | case Bytes = 16; 28 | 29 | /** 30 | * Hexadecimal representation 31 | */ 32 | case Hex = 32; 33 | 34 | /** 35 | * ULID representation 36 | */ 37 | case Ulid = 26; 38 | } 39 | -------------------------------------------------------------------------------- /src/Ulid/Utility/Mask.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Ulid\Utility; 18 | 19 | /** 20 | * @internal 21 | */ 22 | final class Mask 23 | { 24 | /** 25 | * A mask used with functions like {@see strspn()} to validate Crockford base 32 strings 26 | */ 27 | public const CROCKFORD32 = '0123456789abcdefghjkmnpqrstvwxyzABCDEFGHJKMNPQRSTVWXYZ'; 28 | 29 | /** 30 | * A mask used with functions like {@see strspn()} to validate hexadecimal strings 31 | */ 32 | public const HEX = '0123456789abcdefABCDEF'; 33 | 34 | /** 35 | * A mask used with functions like {@see strspn()} to validate string integers 36 | */ 37 | public const INT = '0123456789'; 38 | 39 | /** 40 | * @codeCoverageIgnore 41 | */ 42 | private function __construct() 43 | { 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Ulid/Utility/Validation.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Ulid\Utility; 18 | 19 | use function strspn; 20 | use function strtolower; 21 | use function strtoupper; 22 | use function strtr; 23 | 24 | /** 25 | * This internal trait provides common validation functionality for ULIDs 26 | * 27 | * @internal 28 | */ 29 | trait Validation 30 | { 31 | /** 32 | * Returns true if the given Crockford base 32, hexadecimal, or bytes representation of a ULID is a Max ULID. 33 | */ 34 | private function isMax(string $ulid, ?Format $format): bool 35 | { 36 | return match ($format) { 37 | Format::Ulid => strtoupper($ulid) === '7ZZZZZZZZZZZZZZZZZZZZZZZZZ', 38 | Format::Hex => strtolower($ulid) === 'ffffffffffffffffffffffffffffffff', 39 | Format::Bytes => $ulid === "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", 40 | default => false, 41 | }; 42 | } 43 | 44 | /** 45 | * Returns true if the given Crockford base 32, hexadecimal, or bytes representation of a ULID is a Nil ULID. 46 | */ 47 | private function isNil(string $ulid, ?Format $format): bool 48 | { 49 | return match ($format) { 50 | Format::Ulid => $ulid === '00000000000000000000000000', 51 | Format::Hex => $ulid === '00000000000000000000000000000000', 52 | Format::Bytes => $ulid === "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00", 53 | default => false, 54 | }; 55 | } 56 | 57 | /** 58 | * Returns true if the ULID is valid, according to the given format. 59 | */ 60 | private function isValid(string $ulid, ?Format $format): bool 61 | { 62 | return match ($format) { 63 | Format::Ulid => (static function (string $ulid): bool { 64 | $ulid = strtr($ulid, 'IiLlOo', '111100'); 65 | 66 | return strspn($ulid, Mask::CROCKFORD32) === Format::Ulid->value && $ulid[0] <= '7'; 67 | })($ulid), 68 | Format::Hex => strspn($ulid, Mask::HEX) === Format::Hex->value, 69 | Format::Bytes => true, 70 | default => false, 71 | }; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/UlidFactory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier; 18 | 19 | use DateTimeInterface; 20 | use Identifier\BytesIdentifierFactory; 21 | use Identifier\DateTimeIdentifierFactory; 22 | use Identifier\IntegerIdentifierFactory; 23 | use Identifier\StringIdentifierFactory; 24 | use Ramsey\Identifier\Exception\InvalidArgument; 25 | 26 | /** 27 | * A factory for creating ULIDs. 28 | */ 29 | interface UlidFactory extends 30 | BytesIdentifierFactory, 31 | DateTimeIdentifierFactory, 32 | IntegerIdentifierFactory, 33 | StringIdentifierFactory 34 | { 35 | public function create(): Ulid; 36 | 37 | public function createFromBytes(string $identifier): Ulid; 38 | 39 | public function createFromDateTime(DateTimeInterface $dateTime): Ulid; 40 | 41 | /** 42 | * Creates a new instance of a ULID from the given hexadecimal representation. 43 | * 44 | * @throws InvalidArgument MUST throw if the identifier is not a legal value. 45 | */ 46 | public function createFromHexadecimal(string $identifier): Ulid; 47 | 48 | public function createFromInteger(int | string $identifier): Ulid; 49 | 50 | public function createFromString(string $identifier): Ulid; 51 | } 52 | -------------------------------------------------------------------------------- /src/Uuid.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier; 18 | 19 | use Identifier\BytesIdentifier; 20 | use Identifier\IntegerIdentifier; 21 | use Ramsey\Identifier\Uuid\Variant; 22 | use Ramsey\Identifier\Uuid\Version; 23 | 24 | /** 25 | * A universally unique identifier (UUID). 26 | * 27 | * @link https://www.rfc-editor.org/rfc/rfc9562 RFC 9562: Universally Unique IDentifiers (UUIDs). 28 | */ 29 | interface Uuid extends BytesIdentifier, IntegerIdentifier 30 | { 31 | /** 32 | * Returns the variant of this UUID, describing the layout of the UUID. 33 | * 34 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-4.1 RFC 9562, section 4.1. Variant Field. 35 | */ 36 | public function getVariant(): Variant; 37 | 38 | /** 39 | * Returns the version of this UUID, describing how the UUID was generated. 40 | * 41 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-4.2 RFC 9562, section 4.2. Version Field. 42 | */ 43 | public function getVersion(): Version; 44 | 45 | /** 46 | * Returns a string representation of the UUID encoded as hexadecimal digits. 47 | */ 48 | public function toHexadecimal(): string; 49 | 50 | /** 51 | * Returns the string standard representation of the UUID as a URN. 52 | * 53 | * @link https://www.rfc-editor.org/rfc/rfc9562#figure-4 RFC 9562, Figure 4: Example URN Namespace for UUID. 54 | * @link https://www.rfc-editor.org/rfc/rfc8141 RFC 8141: Uniform Resource Names (URNs). 55 | * 56 | * @return non-empty-string 57 | */ 58 | public function toUrn(): string; 59 | } 60 | -------------------------------------------------------------------------------- /src/Uuid/DceDomain.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | /** 20 | * DCE local domains for version 2, DCE Security UUIDs. 21 | * 22 | * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap11.htm#tagcjh_14_05_01_01 DCE 1.1: Auth & Sec, §11.5.1.1. 23 | */ 24 | enum DceDomain: int 25 | { 26 | /** 27 | * Principal domain. 28 | */ 29 | case Person = 0; 30 | 31 | /** 32 | * Group domain. 33 | */ 34 | case Group = 1; 35 | 36 | /** 37 | * Organization domain. 38 | */ 39 | case Org = 2; 40 | 41 | /** 42 | * Returns the "string name" value, as defined by DCE. 43 | */ 44 | public function dceStringName(): string 45 | { 46 | return match ($this) { 47 | DceDomain::Person => 'person', 48 | DceDomain::Group => 'group', 49 | DceDomain::Org => 'org', 50 | }; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Uuid/MaxUuid.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\Exception\BadMethodCall; 20 | use Ramsey\Identifier\Exception\InvalidArgument; 21 | use Ramsey\Identifier\Uuid; 22 | use Ramsey\Identifier\Uuid\Utility\Format; 23 | use Ramsey\Identifier\Uuid\Utility\Standard; 24 | 25 | use function sprintf; 26 | use function strlen; 27 | 28 | /** 29 | * The Max UUID is a special form of UUID that is specified to have all 128 bits set to one (1). 30 | * 31 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.10 RFC 9562, section 5.10: Max UUID. 32 | */ 33 | final readonly class MaxUuid implements Uuid 34 | { 35 | use Standard; 36 | 37 | private const MAX = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff"; 38 | 39 | /** 40 | * @throws InvalidArgument 41 | */ 42 | public function __construct(private string $uuid = self::MAX) 43 | { 44 | $this->format = Format::tryFrom(strlen($this->uuid)); 45 | 46 | if (!$this->isValid($this->uuid, $this->format)) { 47 | throw new InvalidArgument(sprintf('Invalid Max UUID: "%s"', $this->uuid)); 48 | } 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | * 54 | * According to RFC 9562 sections {@link https://www.rfc-editor.org/rfc/rfc9562#section-4.1 4.1} and 55 | * {@link https://www.rfc-editor.org/rfc/rfc9562#section-5.10 5.10}, the Max UUID falls within the range 56 | * of the future variant. 57 | */ 58 | public function getVariant(): Variant 59 | { 60 | return Variant::Future; 61 | } 62 | 63 | /** 64 | * @throws BadMethodCall 65 | */ 66 | public function getVersion(): never 67 | { 68 | throw new BadMethodCall('Max UUIDs do not have a version field'); 69 | } 70 | 71 | private function isValid(string $uuid, ?Format $format): bool 72 | { 73 | return $this->isMax($uuid, $format); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Uuid/MicrosoftGuidFactory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Brick\Math\BigInteger; 20 | use Brick\Math\Exception\MathException; 21 | use Ramsey\Identifier\Exception\BadMethodCall; 22 | use Ramsey\Identifier\Exception\InvalidArgument; 23 | use Ramsey\Identifier\Service\BytesGenerator\BytesGenerator; 24 | use Ramsey\Identifier\Service\BytesGenerator\RandomBytesGenerator; 25 | use Ramsey\Identifier\Uuid\Utility\Binary; 26 | use Ramsey\Identifier\Uuid\Utility\StandardFactory; 27 | use Ramsey\Identifier\UuidFactory as UuidFactoryInterface; 28 | use Throwable; 29 | 30 | use function sprintf; 31 | use function str_pad; 32 | use function substr; 33 | 34 | use const STR_PAD_LEFT; 35 | 36 | /** 37 | * A factory for creating Microsoft GUIDs. 38 | * 39 | * These GUIDs may either be "reserved Microsoft" variant UUIDs or RFC 9562 UUIDs using the Microsoft GUID binary 40 | * encoding. See {@see MicrosoftGuid} for more information on this encoding. 41 | */ 42 | final readonly class MicrosoftGuidFactory implements UuidFactoryInterface 43 | { 44 | use StandardFactory; 45 | 46 | private Binary $binary; 47 | 48 | /** 49 | * Constructs a factory for creating Microsoft GUIDs. 50 | * 51 | * @param BytesGenerator $bytesGenerator A random generator used to generate bytes; defaults to {@see RandomBytesGenerator}. 52 | */ 53 | public function __construct( 54 | private BytesGenerator $bytesGenerator = new RandomBytesGenerator(), 55 | ) { 56 | $this->binary = new Binary(); 57 | } 58 | 59 | public function create(): MicrosoftGuid 60 | { 61 | $bytes = $this->bytesGenerator->bytes(); 62 | $bytes = $this->binary->applyVersionAndVariant($bytes, Version::Random, Variant::Microsoft); 63 | 64 | return new MicrosoftGuid($this->swapBytes($bytes)); 65 | } 66 | 67 | /** 68 | * @throws InvalidArgument 69 | */ 70 | public function createFromBytes(string $identifier): MicrosoftGuid 71 | { 72 | /** @var MicrosoftGuid */ 73 | return $this->createFromBytesInternal($identifier); 74 | } 75 | 76 | /** 77 | * @throws InvalidArgument 78 | */ 79 | public function createFromHexadecimal(string $identifier): MicrosoftGuid 80 | { 81 | /** @var MicrosoftGuid */ 82 | return $this->createFromHexadecimalInternal($identifier); 83 | } 84 | 85 | /** 86 | * @throws InvalidArgument 87 | */ 88 | public function createFromInteger(int | string $identifier): MicrosoftGuid 89 | { 90 | try { 91 | $bigInteger = BigInteger::of($identifier); 92 | } catch (MathException $exception) { 93 | throw new InvalidArgument(sprintf('Invalid integer: "%s"', $identifier), 0, $exception); 94 | } 95 | 96 | try { 97 | $bytes = str_pad($bigInteger->toBytes(false), 16, "\x00", STR_PAD_LEFT); 98 | 99 | /** @var MicrosoftGuid */ 100 | return $this->createFromBytesInternal($this->swapBytes($bytes)); 101 | } catch (Throwable $exception) { 102 | throw new InvalidArgument(sprintf('Invalid Microsoft GUID: %s', $identifier), 0, $exception); 103 | } 104 | } 105 | 106 | /** 107 | * Returns a "reserved Microsoft" variant Microsoft GUID created from the 108 | * given RFC 9562 UUID 109 | * 110 | * The new GUID returned will be of the "reserved Microsoft" variant. This 111 | * means some bits will change, and the two values will not be equal. 112 | * 113 | * @param UuidV1 | UuidV2 | UuidV3 | UuidV4 | UuidV5 | UuidV6 | UuidV7 | UuidV8 $uuid The UUID to convert to a Microsoft GUID 114 | */ 115 | public function createFromRfc(UuidV1 | UuidV2 | UuidV3 | UuidV4 | UuidV5 | UuidV6 | UuidV7 | UuidV8 $uuid): MicrosoftGuid // phpcs:ignore 116 | { 117 | $bytes = $this->binary->applyVersionAndVariant($uuid->toBytes(), $uuid->getVersion(), Variant::Microsoft); 118 | 119 | return new MicrosoftGuid($this->swapBytes($bytes)); 120 | } 121 | 122 | /** 123 | * @throws InvalidArgument 124 | */ 125 | public function createFromString(string $identifier): MicrosoftGuid 126 | { 127 | /** @var MicrosoftGuid */ 128 | return $this->createFromStringInternal($identifier); 129 | } 130 | 131 | /** 132 | * @codeCoverageIgnore 133 | */ 134 | protected function getVersion(): never 135 | { 136 | throw new BadMethodCall('getVersion() called out of context'); 137 | } 138 | 139 | protected function getUuidClass(): string 140 | { 141 | return MicrosoftGuid::class; 142 | } 143 | 144 | private function swapBytes(string $bytes): string 145 | { 146 | return $bytes[3] . $bytes[2] . $bytes[1] . $bytes[0] 147 | . $bytes[5] . $bytes[4] . $bytes[7] . $bytes[6] 148 | . substr($bytes, 8); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/Uuid/NamespaceId.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\Uuid; 20 | 21 | /** 22 | * Version 3 and 5 UUIDs use name space IDs to ensure uniqueness of name-based identifiers within a unique name space. 23 | * 24 | * RFC 9562 defines name space IDs for names created for the domain name system (DNS), uniform resource locators (URLs), 25 | * ISO object IDs (OIDs) and X.500 distinguished names. This list may be expanded in the future, and it's not intended 26 | * to limit other, specialized name space IDs for different applications. 27 | * 28 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-6.6 RFC 9562, section 6.6. Namespace ID Usage and Allocation. 29 | */ 30 | enum NamespaceId: string 31 | { 32 | /** 33 | * Name string is a fully qualified domain name. 34 | */ 35 | case DNS = '6ba7b810-9dad-11d1-80b4-00c04fd430c8'; 36 | 37 | /** 38 | * Name string is an ISO OID. 39 | */ 40 | case OID = '6ba7b812-9dad-11d1-80b4-00c04fd430c8'; 41 | 42 | /** 43 | * Name string is a URL. 44 | */ 45 | case URL = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'; 46 | 47 | /** 48 | * Name string is an X.500 DN (in DER or text output format). 49 | */ 50 | case X500 = '6ba7b814-9dad-11d1-80b4-00c04fd430c8'; 51 | 52 | /** 53 | * Returns a UUID instance of the namespace. 54 | */ 55 | public function uuid(): Uuid 56 | { 57 | return new UuidV1($this->value); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Uuid/NilUuid.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\Exception\BadMethodCall; 20 | use Ramsey\Identifier\Exception\InvalidArgument; 21 | use Ramsey\Identifier\Uuid; 22 | use Ramsey\Identifier\Uuid\Utility\Format; 23 | use Ramsey\Identifier\Uuid\Utility\Standard; 24 | 25 | use function sprintf; 26 | use function strlen; 27 | 28 | /** 29 | * The Nil UUID is a special form of UUID that is specified to have all 128 bits set to zero (0). 30 | * 31 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.9 RFC 9562, section 5.9. Nil UUID. 32 | */ 33 | final readonly class NilUuid implements Uuid 34 | { 35 | use Standard; 36 | 37 | private const NIL = "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; 38 | 39 | /** 40 | * @throws InvalidArgument 41 | */ 42 | public function __construct(private string $uuid = self::NIL) 43 | { 44 | $this->format = Format::tryFrom(strlen($this->uuid)); 45 | 46 | if (!$this->isValid($this->uuid, $this->format)) { 47 | throw new InvalidArgument(sprintf('Invalid Nil UUID: "%s"', $this->uuid)); 48 | } 49 | } 50 | 51 | /** 52 | * {@inheritDoc} 53 | * 54 | * According to RFC 9562 sections {@link https://www.rfc-editor.org/rfc/rfc9562#section-4.1 4.1} and 55 | * {@link https://www.rfc-editor.org/rfc/rfc9562#section-5.9 5.9}, the Nil UUID falls within the range of the Apollo 56 | * NCS variant. 57 | */ 58 | public function getVariant(): Variant 59 | { 60 | return Variant::Ncs; 61 | } 62 | 63 | /** 64 | * @throws BadMethodCall 65 | */ 66 | public function getVersion(): never 67 | { 68 | throw new BadMethodCall('Nil UUIDs do not have a version field'); 69 | } 70 | 71 | private function isValid(string $uuid, ?Format $format): bool 72 | { 73 | return $this->isNil($uuid, $format); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Uuid/NonstandardUuid.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use JsonSerializable; 20 | use Ramsey\Identifier\Exception\BadMethodCall; 21 | use Ramsey\Identifier\Exception\InvalidArgument; 22 | use Ramsey\Identifier\Uuid; 23 | use Ramsey\Identifier\Uuid\Utility\Format; 24 | use Ramsey\Identifier\Uuid\Utility\Standard; 25 | 26 | use function assert; 27 | use function sprintf; 28 | use function strlen; 29 | 30 | /** 31 | * Nonstandard UUIDs look like UUIDs, but they do not have the variant and version bits set according to RFC 9562. 32 | * 33 | * It is possible a nonstandard UUID was generated according to RFC 9562 but had its bits rearranged for reasons such as 34 | * sortability. Without knowing which rearrangement algorithm was used, it is impossible to determine the UUID's 35 | * original layout, so we treat it as a "nonstandard" UUID. 36 | */ 37 | final readonly class NonstandardUuid implements JsonSerializable, Uuid 38 | { 39 | use Standard; 40 | 41 | private ?Variant $variant; 42 | 43 | /** 44 | * @throws InvalidArgument 45 | */ 46 | public function __construct(private string $uuid) 47 | { 48 | $this->format = Format::tryFrom(strlen($this->uuid)); 49 | $this->variant = $this->getVariantFromUuid($this->uuid, $this->format); 50 | 51 | if (!$this->isValid($this->uuid, $this->format)) { 52 | throw new InvalidArgument(sprintf('Invalid nonstandard UUID: "%s"', $this->uuid)); 53 | } 54 | } 55 | 56 | public function getVariant(): Variant 57 | { 58 | assert($this->variant !== null); 59 | 60 | return $this->variant; 61 | } 62 | 63 | /** 64 | * @throws BadMethodCall 65 | */ 66 | public function getVersion(): never 67 | { 68 | throw new BadMethodCall('Nonstandard UUIDs do not have a version field'); 69 | } 70 | 71 | private function isValid(string $uuid, ?Format $format): bool 72 | { 73 | if (!$this->hasValidFormat($uuid, $format)) { 74 | return false; 75 | } 76 | 77 | if ($this->isMax($uuid, $format) || $this->isNil($uuid, $format)) { 78 | return false; 79 | } 80 | 81 | if ($this->variant !== Variant::Rfc && $this->variant !== Variant::Microsoft) { 82 | return true; 83 | } 84 | 85 | $version = $this->getVersionFromUuid($uuid, $format, $this->variant === Variant::Microsoft); 86 | 87 | // Version 2 UUIDs that do not have a proper domain are nonstandard. 88 | if ($version === 2 && $this->getLocalDomainFromUuid($uuid, $format) === null) { 89 | return true; 90 | } 91 | 92 | return $version < 1 || $version > 8; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Uuid/Utility/Binary.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid\Utility; 18 | 19 | use Ramsey\Identifier\Exception\InvalidArgument; 20 | use Ramsey\Identifier\Uuid\Variant; 21 | use Ramsey\Identifier\Uuid\Version; 22 | 23 | use function pack; 24 | use function strlen; 25 | use function unpack; 26 | 27 | /** 28 | * @internal 29 | */ 30 | final class Binary 31 | { 32 | /** 33 | * Applies the version number and variant field to the 128-bit integer (as a 16-byte string) provided. 34 | * 35 | * @param non-empty-string $bytes A 128-bit integer (16-byte string) to which the version number and variant field 36 | * will be applied, making the number a valid UUID. 37 | * @param Version | null $version The version to apply. 38 | * @param Variant $variant The variant to apply. 39 | * 40 | * @return non-empty-string A 16-byte string with the UUID version and variant applied. 41 | * 42 | * @throws InvalidArgument 43 | */ 44 | public function applyVersionAndVariant( 45 | string $bytes, 46 | ?Version $version, 47 | Variant $variant = Variant::Rfc, 48 | ): string { 49 | if (strlen($bytes) !== 16) { 50 | throw new InvalidArgument('$bytes must be a a 16-byte string'); 51 | } 52 | 53 | /** @var int[] $parts */ 54 | $parts = unpack('n8', $bytes); 55 | 56 | if ($version !== null) { 57 | $parts[4] = $parts[4] & 0x0fff; 58 | $parts[4] |= $version->value << 12; 59 | } 60 | 61 | $parts[5] = match ($variant) { 62 | Variant::Ncs => $parts[5] & 0x7fff, 63 | Variant::Rfc => $parts[5] & 0x3fff | 0x8000, 64 | Variant::Microsoft => $parts[5] & 0x1fff | 0xc000, 65 | Variant::Future => $parts[5] & 0x1fff | 0xe000, 66 | }; 67 | 68 | /** @var non-empty-string */ 69 | return pack('n8', ...$parts); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/Uuid/Utility/Format.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid\Utility; 18 | 19 | /** 20 | * @internal 21 | */ 22 | enum Format: int 23 | { 24 | /** 25 | * Bytes representation. 26 | */ 27 | case Bytes = 16; 28 | 29 | /** 30 | * Hexadecimal representation. 31 | */ 32 | case Hex = 32; 33 | 34 | /** 35 | * String standard representation. 36 | */ 37 | case String = 36; 38 | } 39 | -------------------------------------------------------------------------------- /src/Uuid/Utility/Mask.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid\Utility; 18 | 19 | /** 20 | * @internal 21 | */ 22 | final class Mask 23 | { 24 | /** 25 | * A character mask used with functions like {@see strspn()} to validate hexadecimal strings. 26 | */ 27 | public const HEX = '0123456789abcdefABCDEF'; 28 | 29 | /** 30 | * A character mask used with functions like {@see strspn()} to validate string integers. 31 | */ 32 | public const INT = '0123456789'; 33 | 34 | /** 35 | * @codeCoverageIgnore 36 | */ 37 | private function __construct() 38 | { 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Uuid/Utility/NodeBased.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid\Utility; 18 | 19 | use function substr; 20 | 21 | /** 22 | * This internal trait provides functionality common to node-based UUIDs. 23 | * 24 | * @internal 25 | */ 26 | trait NodeBased 27 | { 28 | /** 29 | * @return non-empty-string 30 | */ 31 | public function getNode(): string 32 | { 33 | /** @var non-empty-string */ 34 | return substr($this->getFormat(Format::Hex), -12); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Uuid/Utility/StandardFactory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid\Utility; 18 | 19 | use Brick\Math\BigInteger; 20 | use Brick\Math\Exception\MathException; 21 | use Ramsey\Identifier\Exception\InvalidArgument; 22 | use Ramsey\Identifier\Uuid; 23 | use Throwable; 24 | 25 | use function sprintf; 26 | use function str_pad; 27 | 28 | use const STR_PAD_LEFT; 29 | 30 | /** 31 | * This internal trait provides common factory functionality for UUIDs. 32 | * 33 | * @internal 34 | */ 35 | trait StandardFactory 36 | { 37 | use Validation; 38 | 39 | /** 40 | * Returns the name of the UUID class to use when instantiating UUID instances from this trait. 41 | * 42 | * @return class-string 43 | */ 44 | abstract protected function getUuidClass(): string; 45 | 46 | /** 47 | * @throws InvalidArgument 48 | */ 49 | private function createFromBytesInternal(string $identifier): Uuid 50 | { 51 | if ($this->hasValidFormat($identifier, Format::Bytes)) { 52 | /** @var Uuid */ 53 | return new ($this->getUuidClass())($identifier); 54 | } 55 | 56 | throw new InvalidArgument('Identifier must be a 16-byte string'); 57 | } 58 | 59 | /** 60 | * @throws InvalidArgument 61 | */ 62 | private function createFromHexadecimalInternal(string $identifier): Uuid 63 | { 64 | if ($this->hasValidFormat($identifier, Format::Hex)) { 65 | /** @var Uuid */ 66 | return new ($this->getUuidClass())($identifier); 67 | } 68 | 69 | throw new InvalidArgument('Identifier must be a 32-character hexadecimal string'); 70 | } 71 | 72 | /** 73 | * The minimum integer value for a version 1 UUID is 75,567,087,097,951,178,194,944. As such, there's no need to use 74 | * better performing math for integers less than PHP_INT_MAX, since those integers can never be valid UUIDs. 75 | * 76 | * @throws InvalidArgument 77 | */ 78 | private function createFromIntegerInternal(int | string $identifier): Uuid 79 | { 80 | try { 81 | $bigInteger = BigInteger::of($identifier); 82 | } catch (MathException $exception) { 83 | throw new InvalidArgument(sprintf('Invalid integer: "%s"', $identifier), 0, $exception); 84 | } 85 | 86 | try { 87 | return $this->createFromBytesInternal(str_pad($bigInteger->toBytes(false), 16, "\x00", STR_PAD_LEFT)); 88 | } catch (Throwable $exception) { 89 | throw new InvalidArgument( 90 | sprintf('Invalid version %d UUID: %s', $this->getVersion()->value, $identifier), 91 | 0, 92 | $exception, 93 | ); 94 | } 95 | } 96 | 97 | /** 98 | * @throws InvalidArgument 99 | */ 100 | private function createFromStringInternal(string $identifier): Uuid 101 | { 102 | if ($this->hasValidFormat($identifier, Format::String)) { 103 | /** @var Uuid */ 104 | return new ($this->getUuidClass())($identifier); 105 | } 106 | 107 | throw new InvalidArgument('Identifier must be a UUID in string standard representation'); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Uuid/Utility/TimeBased.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid\Utility; 18 | 19 | use DateTimeImmutable; 20 | 21 | /** 22 | * This internal trait provides functionality common to time-based UUIDs. 23 | * 24 | * @internal 25 | */ 26 | trait TimeBased 27 | { 28 | public function getDateTime(): DateTimeImmutable 29 | { 30 | return (new Time())->getDateTimeForUuid($this); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Uuid/UuidV1.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\NodeBasedUuid; 20 | use Ramsey\Identifier\TimeBasedUuid; 21 | use Ramsey\Identifier\Uuid\Utility\NodeBased; 22 | use Ramsey\Identifier\Uuid\Utility\Standard; 23 | use Ramsey\Identifier\Uuid\Utility\TimeBased; 24 | 25 | /** 26 | * Gregorian time, or version 1, UUIDs include timestamp, clock sequence, and node values that are combined into a 27 | * 128-bit unsigned integer. 28 | * 29 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.1 RFC 9562, section 5.1. UUID Version 1. 30 | */ 31 | final readonly class UuidV1 implements NodeBasedUuid, TimeBasedUuid 32 | { 33 | use Standard; 34 | use NodeBased; 35 | use TimeBased; 36 | 37 | public function getVersion(): Version 38 | { 39 | return Version::GregorianTime; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Uuid/UuidV1Factory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use DateTimeInterface; 20 | use Psr\Clock\ClockInterface as Clock; 21 | use Ramsey\Identifier\Exception\InvalidArgument; 22 | use Ramsey\Identifier\Service\Clock\ClockSequence; 23 | use Ramsey\Identifier\Service\Clock\RandomClockSequence; 24 | use Ramsey\Identifier\Service\Clock\SystemClock; 25 | use Ramsey\Identifier\Service\Nic\Nic; 26 | use Ramsey\Identifier\Service\Nic\RandomNic; 27 | use Ramsey\Identifier\Service\Nic\StaticNic; 28 | use Ramsey\Identifier\TimeBasedUuidFactory; 29 | use Ramsey\Identifier\Uuid\Utility\Binary; 30 | use Ramsey\Identifier\Uuid\Utility\StandardFactory; 31 | use Ramsey\Identifier\Uuid\Utility\Time; 32 | 33 | use function hex2bin; 34 | use function pack; 35 | use function sprintf; 36 | use function substr; 37 | 38 | /** 39 | * A factory for creating version 1, Gregorian time UUIDs. 40 | */ 41 | final class UuidV1Factory implements TimeBasedUuidFactory 42 | { 43 | use StandardFactory; 44 | 45 | /** 46 | * The maximum value of the clock sequence before it must roll over to zero. 47 | */ 48 | private const CLOCK_SEQ_MAX = 0x4000; 49 | 50 | private readonly Binary $binary; 51 | private readonly Time $time; 52 | 53 | /** 54 | * Constructs a factory for creating version 1, Gregorian time UUIDs 55 | * 56 | * @param Clock $clock A clock used to provide a date-time instance; defaults to {@see SystemClock}. 57 | * @param Nic $nic A NIC that provides the system MAC address value; defaults to {@see RandomNic}. 58 | * @param ClockSequence $sequence A sequence that provides a clock sequence value to prevent collisions; defaults to {@see RandomClockSequence}. 59 | */ 60 | public function __construct( 61 | private readonly Clock $clock = new SystemClock(), 62 | private readonly Nic $nic = new RandomNic(), 63 | private readonly ClockSequence $sequence = new RandomClockSequence(), 64 | ) { 65 | $this->binary = new Binary(); 66 | $this->time = new Time(); 67 | } 68 | 69 | /** 70 | * @param Nic | int<0, max> | non-empty-string | null $node A 48-bit integer or hexadecimal string representing the 71 | * hardware address of the machine where this identifier was generated. 72 | * @param int<0, 16383> | null $clockSequence A 14-bit number used to help avoid duplicates that could arise when 73 | * the clock is set backwards in time or if the node ID changes. 74 | * @param DateTimeInterface | null $dateTime A date-time to use when creating the identifier. 75 | * 76 | * @throws InvalidArgument 77 | */ 78 | public function create( 79 | Nic | int | string | null $node = null, 80 | ?int $clockSequence = null, 81 | ?DateTimeInterface $dateTime = null, 82 | ): UuidV1 { 83 | if ($node === null) { 84 | $node = $this->nic->address(); 85 | } elseif ($node instanceof Nic) { 86 | $node = $node->address(); 87 | } else { 88 | $node = (new StaticNic($node))->address(); 89 | } 90 | 91 | $dateTime = $dateTime ?? $this->clock->now(); 92 | $clockSequence = ($clockSequence ?? $this->sequence->next($node, $dateTime)) % self::CLOCK_SEQ_MAX; 93 | 94 | $timeBytes = $this->time->getTimeBytesForGregorianEpoch($dateTime); 95 | 96 | /** @var non-empty-string $bytes */ 97 | $bytes = substr($timeBytes, -4) 98 | . substr($timeBytes, 2, 2) 99 | . substr($timeBytes, 0, 2) 100 | . pack('n', $clockSequence) 101 | . hex2bin(sprintf('%012s', $node)); 102 | 103 | $bytes = $this->binary->applyVersionAndVariant($bytes, Version::GregorianTime); 104 | 105 | return new UuidV1($bytes); 106 | } 107 | 108 | /** 109 | * @throws InvalidArgument 110 | */ 111 | public function createFromBytes(string $identifier): UuidV1 112 | { 113 | /** @var UuidV1 */ 114 | return $this->createFromBytesInternal($identifier); 115 | } 116 | 117 | /** 118 | * @throws InvalidArgument 119 | */ 120 | public function createFromDateTime(DateTimeInterface $dateTime): UuidV1 121 | { 122 | return $this->create(dateTime: $dateTime); 123 | } 124 | 125 | /** 126 | * @throws InvalidArgument 127 | */ 128 | public function createFromHexadecimal(string $identifier): UuidV1 129 | { 130 | /** @var UuidV1 */ 131 | return $this->createFromHexadecimalInternal($identifier); 132 | } 133 | 134 | /** 135 | * @throws InvalidArgument 136 | */ 137 | public function createFromInteger(int | string $identifier): UuidV1 138 | { 139 | /** @var UuidV1 */ 140 | return $this->createFromIntegerInternal($identifier); 141 | } 142 | 143 | /** 144 | * @throws InvalidArgument 145 | */ 146 | public function createFromString(string $identifier): UuidV1 147 | { 148 | /** @var UuidV1 */ 149 | return $this->createFromStringInternal($identifier); 150 | } 151 | 152 | protected function getVersion(): Version 153 | { 154 | return Version::GregorianTime; 155 | } 156 | 157 | protected function getUuidClass(): string 158 | { 159 | return UuidV1::class; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Uuid/UuidV2.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\NodeBasedUuid; 20 | use Ramsey\Identifier\TimeBasedUuid; 21 | use Ramsey\Identifier\Uuid\Utility\Format; 22 | use Ramsey\Identifier\Uuid\Utility\NodeBased; 23 | use Ramsey\Identifier\Uuid\Utility\Standard; 24 | use Ramsey\Identifier\Uuid\Utility\TimeBased; 25 | 26 | use function hexdec; 27 | use function substr; 28 | 29 | /** 30 | * DCE Security version, or version 2, UUIDs include local domain identifier, local ID for the specified domain, and 31 | * node values that are combined into a 128-bit unsigned integer. 32 | * 33 | * It is important to note that a version 2 UUID suffers from some loss of timestamp fidelity, due to replacing the 34 | * `time_low` field with the local identifier. When constructing the timestamp value for date purposes, we replace the 35 | * local identifier bits with zeros. As a result, the timestamp can be off by a range of 0 to 429.4967295 seconds (or 7 36 | * minutes, 9 seconds, and 496,730 microseconds). 37 | * 38 | * You might notice this value directly corresponds to 2^32–1, or 0xffffffff. The local identifier is 32-bits, and we 39 | * have set each of these bits to 0, so the maximum range of timestamp drift is 0x00000000 to 0xffffffff (counted in 40 | * 100-nanosecond intervals). 41 | * 42 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.2 RFC 9562, section 5.2. UUID Version 2. 43 | * @link https://publications.opengroup.org/c311 DCE 1.1: Authentication and Security Services. 44 | * @link https://publications.opengroup.org/c706 DCE 1.1: Remote Procedure Call. 45 | * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01 DCE 1.1: Auth & Sec, §5.2.1.1. 46 | * @link https://pubs.opengroup.org/onlinepubs/9696989899/chap11.htm#tagcjh_14_05_01_01 DCE 1.1: Auth & Sec, §11.5.1.1. 47 | * @link https://pubs.opengroup.org/onlinepubs/9629399/apdxa.htm DCE 1.1: RPC, Appendix A. 48 | * @link https://github.com/google/uuid Go package for UUIDs (includes DCE implementation). 49 | */ 50 | final readonly class UuidV2 implements NodeBasedUuid, TimeBasedUuid 51 | { 52 | use Standard { 53 | isValid as private baseIsValid; 54 | } 55 | use NodeBased; 56 | use TimeBased; 57 | 58 | /** 59 | * Returns the local domain to which the local identifier belongs. 60 | * 61 | * For example, if the local domain is {@see DceDomain::Person}, then the local identifier should indicate the ID of 62 | * a person's account on the local host. On POSIX systems, this is usually the UID. 63 | */ 64 | public function getLocalDomain(): DceDomain 65 | { 66 | /** @var DceDomain */ 67 | return $this->getLocalDomainFromUuid($this->uuid, $this->format); 68 | } 69 | 70 | /** 71 | * Returns an identifier meaningful to the local host where this UUID was created. 72 | * 73 | * The type of this identifier is indicated by the domain returned from {@see self::getLocalDomain()}. For example, 74 | * if the domain is {@see DceDomain::Group}, this identifier is a group ID on the local host. On POSIX systems, 75 | * this is usually the GID. 76 | */ 77 | public function getLocalIdentifier(): int 78 | { 79 | return (int) hexdec(substr($this->getFormat(Format::String), 0, 8)); 80 | } 81 | 82 | public function getVersion(): Version 83 | { 84 | return Version::DceSecurity; 85 | } 86 | 87 | protected function isValid(string $uuid, ?Format $format): bool 88 | { 89 | return $this->baseIsValid($uuid, $format) 90 | && $this->getLocalDomainFromUuid($uuid, $format) !== null; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Uuid/UuidV3.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\Uuid; 20 | use Ramsey\Identifier\Uuid\Utility\Standard; 21 | 22 | /** 23 | * Version 3 UUIDs are named-based, using a combination of a namespace and name that are hashed into a 128-bit unsigned 24 | * integer using the MD5 hashing algorithm. 25 | * 26 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.3 RFC 9562, section 5.3. UUID Version 3. 27 | */ 28 | final readonly class UuidV3 implements Uuid 29 | { 30 | use Standard; 31 | 32 | public function getVersion(): Version 33 | { 34 | return Version::NameMd5; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Uuid/UuidV3Factory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\Exception\InvalidArgument; 20 | use Ramsey\Identifier\Uuid; 21 | use Ramsey\Identifier\Uuid\Utility\Binary; 22 | use Ramsey\Identifier\Uuid\Utility\StandardFactory; 23 | use Ramsey\Identifier\UuidFactory as UuidFactoryInterface; 24 | 25 | use function hash; 26 | 27 | /** 28 | * A factory for creating version 3, name-based (MD5) UUIDs. 29 | */ 30 | final class UuidV3Factory implements UuidFactoryInterface 31 | { 32 | use StandardFactory; 33 | 34 | private readonly Binary $binary; 35 | 36 | public function __construct() 37 | { 38 | $this->binary = new Binary(); 39 | } 40 | 41 | /** 42 | * @throws InvalidArgument 43 | */ 44 | public function create(?Uuid $namespace = null, ?string $name = null): UuidV3 45 | { 46 | if ($namespace === null) { 47 | throw new InvalidArgument('$namespace cannot be null when creating version 3 UUIDs'); 48 | } 49 | 50 | if ($name === null) { 51 | throw new InvalidArgument('$name cannot be null when creating version 3 UUIDs'); 52 | } 53 | 54 | $bytes = hash('md5', $namespace->toBytes() . $name, true); 55 | $bytes = $this->binary->applyVersionAndVariant($bytes, Version::NameMd5); 56 | 57 | return new UuidV3($bytes); 58 | } 59 | 60 | /** 61 | * @throws InvalidArgument 62 | */ 63 | public function createFromBytes(string $identifier): UuidV3 64 | { 65 | /** @var UuidV3 */ 66 | return $this->createFromBytesInternal($identifier); 67 | } 68 | 69 | /** 70 | * @throws InvalidArgument 71 | */ 72 | public function createFromHexadecimal(string $identifier): UuidV3 73 | { 74 | /** @var UuidV3 */ 75 | return $this->createFromHexadecimalInternal($identifier); 76 | } 77 | 78 | /** 79 | * @throws InvalidArgument 80 | */ 81 | public function createFromInteger(int | string $identifier): UuidV3 82 | { 83 | /** @var UuidV3 */ 84 | return $this->createFromIntegerInternal($identifier); 85 | } 86 | 87 | /** 88 | * @throws InvalidArgument 89 | */ 90 | public function createFromString(string $identifier): UuidV3 91 | { 92 | /** @var UuidV3 */ 93 | return $this->createFromStringInternal($identifier); 94 | } 95 | 96 | protected function getVersion(): Version 97 | { 98 | return Version::NameMd5; 99 | } 100 | 101 | protected function getUuidClass(): string 102 | { 103 | return UuidV3::class; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Uuid/UuidV4.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\Uuid; 20 | use Ramsey\Identifier\Uuid\Utility\Standard; 21 | 22 | /** 23 | * Random, or version 4, UUIDs are randomly generated 128-bit integers. 24 | * 25 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.4 RFC 9562, section 5.4. UUID Version 4. 26 | */ 27 | final readonly class UuidV4 implements Uuid 28 | { 29 | use Standard; 30 | 31 | public function getVersion(): Version 32 | { 33 | return Version::Random; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Uuid/UuidV4Factory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\Exception\InvalidArgument; 20 | use Ramsey\Identifier\Service\BytesGenerator\BytesGenerator; 21 | use Ramsey\Identifier\Service\BytesGenerator\RandomBytesGenerator; 22 | use Ramsey\Identifier\Uuid\Utility\Binary; 23 | use Ramsey\Identifier\Uuid\Utility\StandardFactory; 24 | use Ramsey\Identifier\UuidFactory as UuidFactoryInterface; 25 | 26 | /** 27 | * A factory for creating version 4, random UUIDs. 28 | */ 29 | final class UuidV4Factory implements UuidFactoryInterface 30 | { 31 | use StandardFactory; 32 | 33 | private readonly Binary $binary; 34 | 35 | /** 36 | * Constructs a factory for creating version 4, random UUIDs. 37 | * 38 | * @param BytesGenerator $bytesGenerator A random generator used to generate bytes; defaults to {@see RandomBytesGenerator}. 39 | */ 40 | public function __construct(private readonly BytesGenerator $bytesGenerator = new RandomBytesGenerator()) 41 | { 42 | $this->binary = new Binary(); 43 | } 44 | 45 | public function create(): UuidV4 46 | { 47 | $bytes = $this->bytesGenerator->bytes(); 48 | $bytes = $this->binary->applyVersionAndVariant($bytes, Version::Random); 49 | 50 | return new UuidV4($bytes); 51 | } 52 | 53 | /** 54 | * @throws InvalidArgument 55 | */ 56 | public function createFromBytes(string $identifier): UuidV4 57 | { 58 | /** @var UuidV4 */ 59 | return $this->createFromBytesInternal($identifier); 60 | } 61 | 62 | /** 63 | * @throws InvalidArgument 64 | */ 65 | public function createFromHexadecimal(string $identifier): UuidV4 66 | { 67 | /** @var UuidV4 */ 68 | return $this->createFromHexadecimalInternal($identifier); 69 | } 70 | 71 | /** 72 | * @throws InvalidArgument 73 | */ 74 | public function createFromInteger(int | string $identifier): UuidV4 75 | { 76 | /** @var UuidV4 */ 77 | return $this->createFromIntegerInternal($identifier); 78 | } 79 | 80 | /** 81 | * @throws InvalidArgument 82 | */ 83 | public function createFromString(string $identifier): UuidV4 84 | { 85 | /** @var UuidV4 */ 86 | return $this->createFromStringInternal($identifier); 87 | } 88 | 89 | protected function getVersion(): Version 90 | { 91 | return Version::Random; 92 | } 93 | 94 | protected function getUuidClass(): string 95 | { 96 | return UuidV4::class; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Uuid/UuidV5.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\Uuid; 20 | use Ramsey\Identifier\Uuid\Utility\Standard; 21 | 22 | /** 23 | * Version 5 UUIDs are named-based, using a combination of a namespace and name that are hashed into a 128-bit unsigned 24 | * integer using the SHA-1 hashing algorithm. 25 | * 26 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.5 RFC 9562, section 5.5. UUID Version 5. 27 | */ 28 | final readonly class UuidV5 implements Uuid 29 | { 30 | use Standard; 31 | 32 | public function getVersion(): Version 33 | { 34 | return Version::NameSha1; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Uuid/UuidV5Factory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\Exception\InvalidArgument; 20 | use Ramsey\Identifier\Uuid; 21 | use Ramsey\Identifier\Uuid\Utility\Binary; 22 | use Ramsey\Identifier\Uuid\Utility\StandardFactory; 23 | use Ramsey\Identifier\UuidFactory as UuidFactoryInterface; 24 | 25 | use function hash; 26 | use function substr; 27 | 28 | /** 29 | * A factory for creating version 5, name-based (SHA-1) UUIDs. 30 | */ 31 | final class UuidV5Factory implements UuidFactoryInterface 32 | { 33 | use StandardFactory; 34 | 35 | private readonly Binary $binary; 36 | 37 | public function __construct() 38 | { 39 | $this->binary = new Binary(); 40 | } 41 | 42 | /** 43 | * @throws InvalidArgument 44 | */ 45 | public function create(?Uuid $namespace = null, ?string $name = null): UuidV5 46 | { 47 | if ($namespace === null) { 48 | throw new InvalidArgument('$namespace cannot be null when creating version 5 UUIDs'); 49 | } 50 | 51 | if ($name === null) { 52 | throw new InvalidArgument('$name cannot be null when creating version 5 UUIDs'); 53 | } 54 | 55 | /** @var non-empty-string $bytes */ 56 | $bytes = substr(hash('sha1', $namespace->toBytes() . $name, true), 0, 16); 57 | $bytes = $this->binary->applyVersionAndVariant($bytes, Version::NameSha1); 58 | 59 | return new UuidV5($bytes); 60 | } 61 | 62 | /** 63 | * @throws InvalidArgument 64 | */ 65 | public function createFromBytes(string $identifier): UuidV5 66 | { 67 | /** @var UuidV5 */ 68 | return $this->createFromBytesInternal($identifier); 69 | } 70 | 71 | /** 72 | * @throws InvalidArgument 73 | */ 74 | public function createFromHexadecimal(string $identifier): UuidV5 75 | { 76 | /** @var UuidV5 */ 77 | return $this->createFromHexadecimalInternal($identifier); 78 | } 79 | 80 | /** 81 | * @throws InvalidArgument 82 | */ 83 | public function createFromInteger(int | string $identifier): UuidV5 84 | { 85 | /** @var UuidV5 */ 86 | return $this->createFromIntegerInternal($identifier); 87 | } 88 | 89 | /** 90 | * @throws InvalidArgument 91 | */ 92 | public function createFromString(string $identifier): UuidV5 93 | { 94 | /** @var UuidV5 */ 95 | return $this->createFromStringInternal($identifier); 96 | } 97 | 98 | protected function getVersion(): Version 99 | { 100 | return Version::NameSha1; 101 | } 102 | 103 | protected function getUuidClass(): string 104 | { 105 | return UuidV5::class; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Uuid/UuidV6.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\NodeBasedUuid; 20 | use Ramsey\Identifier\TimeBasedUuid; 21 | use Ramsey\Identifier\Uuid\Utility\NodeBased; 22 | use Ramsey\Identifier\Uuid\Utility\Standard; 23 | use Ramsey\Identifier\Uuid\Utility\TimeBased; 24 | 25 | /** 26 | * Reordered Gregorian time, or version 6, UUIDs include timestamp, clock sequence, and node values that are combined 27 | * into a 128-bit unsigned integer. 28 | * 29 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.6 RFC 9562, section 5.6. UUID Version 6. 30 | */ 31 | final readonly class UuidV6 implements NodeBasedUuid, TimeBasedUuid 32 | { 33 | use Standard; 34 | use NodeBased; 35 | use TimeBased; 36 | 37 | public function getVersion(): Version 38 | { 39 | return Version::ReorderedGregorianTime; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Uuid/UuidV6Factory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use DateTimeInterface; 20 | use Psr\Clock\ClockInterface as Clock; 21 | use Ramsey\Identifier\Exception\InvalidArgument; 22 | use Ramsey\Identifier\Service\Clock\ClockSequence; 23 | use Ramsey\Identifier\Service\Clock\RandomClockSequence; 24 | use Ramsey\Identifier\Service\Clock\SystemClock; 25 | use Ramsey\Identifier\Service\Nic\Nic; 26 | use Ramsey\Identifier\Service\Nic\RandomNic; 27 | use Ramsey\Identifier\Service\Nic\StaticNic; 28 | use Ramsey\Identifier\TimeBasedUuidFactory; 29 | use Ramsey\Identifier\Uuid\Utility\Binary; 30 | use Ramsey\Identifier\Uuid\Utility\StandardFactory; 31 | use Ramsey\Identifier\Uuid\Utility\Time; 32 | 33 | use function bin2hex; 34 | use function hex2bin; 35 | use function pack; 36 | use function sprintf; 37 | use function substr; 38 | 39 | /** 40 | * A factory for creating version 6, reordered Gregorian time UUIDs. 41 | */ 42 | final class UuidV6Factory implements TimeBasedUuidFactory 43 | { 44 | use StandardFactory; 45 | 46 | /** 47 | * The maximum value of the clock sequence before it must roll over to zero. 48 | */ 49 | private const CLOCK_SEQ_MAX = 16_384; 50 | 51 | private readonly Binary $binary; 52 | private readonly Time $time; 53 | 54 | /** 55 | * Constructs a factory for creating version 6, reordered time UUIDs. 56 | * 57 | * @param Clock $clock A clock used to provide a date-time instance; defaults to {@see SystemClock}. 58 | * @param Nic $nic A NIC that provides the system MAC address value; defaults to {@see RandomNic}. 59 | * @param ClockSequence $sequence A sequence that provides a clock sequence value to prevent collisions; defaults to {@see RandomClockSequence}. 60 | */ 61 | public function __construct( 62 | private readonly Clock $clock = new SystemClock(), 63 | private readonly Nic $nic = new RandomNic(), 64 | private readonly ClockSequence $sequence = new RandomClockSequence(), 65 | ) { 66 | $this->binary = new Binary(); 67 | $this->time = new Time(); 68 | } 69 | 70 | /** 71 | * @param int<0, max> | non-empty-string | null $node A 48-bit integer or hexadecimal string representing the 72 | * hardware address of the machine where this identifier was generated. 73 | * @param int<0, 16383> | null $clockSequence A 14-bit number used to help avoid duplicates that could arise when 74 | * the clock is set backwards in time or if the node ID changes. 75 | * @param DateTimeInterface | null $dateTime A date-time to use when creating the identifier. 76 | * 77 | * @throws InvalidArgument 78 | */ 79 | public function create( 80 | int | string | null $node = null, 81 | ?int $clockSequence = null, 82 | ?DateTimeInterface $dateTime = null, 83 | ): UuidV6 { 84 | $node = $node === null ? $this->nic->address() : (new StaticNic($node))->address(); 85 | $dateTime = $dateTime ?? $this->clock->now(); 86 | $clockSequence = ($clockSequence ?? $this->sequence->next($node, $dateTime)) % self::CLOCK_SEQ_MAX; 87 | 88 | $timeBytes = $this->time->getTimeBytesForGregorianEpoch($dateTime); 89 | $timeHex = bin2hex($timeBytes); 90 | 91 | /** @var non-empty-string $bytes */ 92 | $bytes = hex2bin(substr($timeHex, 1, 12) . '0' . substr($timeHex, -3)) 93 | . pack('n', $clockSequence) 94 | . hex2bin(sprintf('%012s', $node)); 95 | 96 | $bytes = $this->binary->applyVersionAndVariant($bytes, Version::ReorderedGregorianTime); 97 | 98 | return new UuidV6($bytes); 99 | } 100 | 101 | /** 102 | * @throws InvalidArgument 103 | */ 104 | public function createFromBytes(string $identifier): UuidV6 105 | { 106 | /** @var UuidV6 */ 107 | return $this->createFromBytesInternal($identifier); 108 | } 109 | 110 | /** 111 | * @throws InvalidArgument 112 | */ 113 | public function createFromDateTime(DateTimeInterface $dateTime): UuidV6 114 | { 115 | return $this->create(dateTime: $dateTime); 116 | } 117 | 118 | /** 119 | * @throws InvalidArgument 120 | */ 121 | public function createFromHexadecimal(string $identifier): UuidV6 122 | { 123 | /** @var UuidV6 */ 124 | return $this->createFromHexadecimalInternal($identifier); 125 | } 126 | 127 | /** 128 | * @throws InvalidArgument 129 | */ 130 | public function createFromInteger(int | string $identifier): UuidV6 131 | { 132 | /** @var UuidV6 */ 133 | return $this->createFromIntegerInternal($identifier); 134 | } 135 | 136 | /** 137 | * @throws InvalidArgument 138 | */ 139 | public function createFromString(string $identifier): UuidV6 140 | { 141 | /** @var UuidV6 */ 142 | return $this->createFromStringInternal($identifier); 143 | } 144 | 145 | protected function getVersion(): Version 146 | { 147 | return Version::ReorderedGregorianTime; 148 | } 149 | 150 | protected function getUuidClass(): string 151 | { 152 | return UuidV6::class; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Uuid/UuidV7.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\TimeBasedUuid; 20 | use Ramsey\Identifier\Uuid\Utility\Standard; 21 | use Ramsey\Identifier\Uuid\Utility\TimeBased; 22 | 23 | /** 24 | * Unix Epoch time, or version 7, UUIDs include a timestamp in milliseconds since the Unix Epoch. 25 | * 26 | * Version 7 UUIDs are designed to be monotonically increasing and sortable. 27 | * 28 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.7 RFC 9562, section 5.7. UUID Version 7. 29 | */ 30 | final readonly class UuidV7 implements TimeBasedUuid 31 | { 32 | use Standard; 33 | use TimeBased; 34 | 35 | public function getVersion(): Version 36 | { 37 | return Version::UnixTime; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Uuid/UuidV7Factory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use DateTimeInterface; 20 | use Ramsey\Identifier\Exception\InvalidArgument; 21 | use Ramsey\Identifier\Service\BytesGenerator\BytesGenerator; 22 | use Ramsey\Identifier\Service\BytesGenerator\MonotonicBytesGenerator; 23 | use Ramsey\Identifier\TimeBasedUuidFactory; 24 | use Ramsey\Identifier\Uuid\Utility\Binary; 25 | use Ramsey\Identifier\Uuid\Utility\StandardFactory; 26 | 27 | use function sprintf; 28 | 29 | /** 30 | * A factory for creating version 7, Unix Epoch time UUIDs. 31 | */ 32 | final class UuidV7Factory implements TimeBasedUuidFactory 33 | { 34 | use StandardFactory; 35 | 36 | private readonly Binary $binary; 37 | 38 | /** 39 | * Constructs a factory for creating version 7, Unix Epoch time UUIDs. 40 | * 41 | * @param BytesGenerator $bytesGenerator A generator used to generate bytes for a version 7 UUID; defaults to {@see MonotonicBytesGenerator}. 42 | */ 43 | public function __construct( 44 | private readonly BytesGenerator $bytesGenerator = new MonotonicBytesGenerator(), 45 | ) { 46 | $this->binary = new Binary(); 47 | } 48 | 49 | /** 50 | * @param DateTimeInterface | null $dateTime A date-time to use when creating the identifier. 51 | * 52 | * @throws InvalidArgument 53 | */ 54 | public function create(?DateTimeInterface $dateTime = null): UuidV7 55 | { 56 | if ($dateTime !== null && $dateTime->getTimestamp() < 0) { 57 | throw new InvalidArgument('Timestamp may not be earlier than the Unix Epoch'); 58 | } elseif ($dateTime !== null && (int) $dateTime->format('Uv') > 0x000ffffffffffff) { 59 | throw new InvalidArgument(sprintf( 60 | 'The date exceeds the maximum value allowed for Unix Epoch time UUIDs: %s', 61 | $dateTime->format('Y-m-d H:i:s.u P'), 62 | )); 63 | } 64 | 65 | $bytes = $this->bytesGenerator->bytes(dateTime: $dateTime); 66 | $bytes = $this->binary->applyVersionAndVariant($bytes, Version::UnixTime); 67 | 68 | return new UuidV7($bytes); 69 | } 70 | 71 | /** 72 | * @throws InvalidArgument 73 | */ 74 | public function createFromBytes(string $identifier): UuidV7 75 | { 76 | /** @var UuidV7 */ 77 | return $this->createFromBytesInternal($identifier); 78 | } 79 | 80 | /** 81 | * @throws InvalidArgument 82 | */ 83 | public function createFromDateTime(DateTimeInterface $dateTime): UuidV7 84 | { 85 | return $this->create(dateTime: $dateTime); 86 | } 87 | 88 | /** 89 | * @throws InvalidArgument 90 | */ 91 | public function createFromHexadecimal(string $identifier): UuidV7 92 | { 93 | /** @var UuidV7 */ 94 | return $this->createFromHexadecimalInternal($identifier); 95 | } 96 | 97 | /** 98 | * @throws InvalidArgument 99 | */ 100 | public function createFromInteger(int | string $identifier): UuidV7 101 | { 102 | /** @var UuidV7 */ 103 | return $this->createFromIntegerInternal($identifier); 104 | } 105 | 106 | /** 107 | * @throws InvalidArgument 108 | */ 109 | public function createFromString(string $identifier): UuidV7 110 | { 111 | /** @var UuidV7 */ 112 | return $this->createFromStringInternal($identifier); 113 | } 114 | 115 | protected function getVersion(): Version 116 | { 117 | return Version::UnixTime; 118 | } 119 | 120 | protected function getUuidClass(): string 121 | { 122 | return UuidV7::class; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/Uuid/UuidV8.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\Uuid; 20 | use Ramsey\Identifier\Uuid\Utility\Standard; 21 | 22 | /** 23 | * Version 8, custom format UUIDs provide an RFC 9562 compatible format for experimental or vendor-specific uses. 24 | * 25 | * The only requirement for version 8 UUIDs is that the version and variant bits must be set. Otherwise, implementations 26 | * are free to set the other bits according to their needs. As a result, the uniqueness of version 8 UUIDs is 27 | * implementation-specific and should not be assumed. 28 | * 29 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-5.8 RFC 9562, section 5.8. UUID Version 8. 30 | */ 31 | final readonly class UuidV8 implements Uuid 32 | { 33 | use Standard; 34 | 35 | public function getVersion(): Version 36 | { 37 | return Version::Custom; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Uuid/UuidV8Factory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | use Ramsey\Identifier\Exception\InvalidArgument; 20 | use Ramsey\Identifier\Uuid\Utility\Binary; 21 | use Ramsey\Identifier\Uuid\Utility\StandardFactory; 22 | use Ramsey\Identifier\UuidFactory as UuidFactoryInterface; 23 | 24 | use function strlen; 25 | 26 | /** 27 | * A factory for creating version 8, custom format UUIDs. 28 | */ 29 | final class UuidV8Factory implements UuidFactoryInterface 30 | { 31 | use StandardFactory; 32 | 33 | private readonly Binary $binary; 34 | 35 | public function __construct() 36 | { 37 | $this->binary = new Binary(); 38 | } 39 | 40 | /** 41 | * Creates a new instance of an identifier. 42 | * 43 | * The bytes provided may contain any value according to your application's needs. Be aware, however, that other 44 | * applications may not understand the semantics of the value. 45 | * 46 | * @param string | null $bytes A 16-byte octet string. This is an open blob of data that you may fill with 128 bits 47 | * of information. Be aware, however, bits 48 through 51 will be replaced with the UUID version field, and bits 48 | * 64 and 65 will be replaced with the UUID variant. You MUST NOT rely on these bits for your application needs. 49 | * 50 | * @throws InvalidArgument if `$bytes` is null or is not a 16-byte octet string. 51 | */ 52 | public function create(?string $bytes = null): UuidV8 53 | { 54 | if ($bytes === null) { 55 | throw new InvalidArgument('$bytes cannot be null when creating version 8 UUIDs'); 56 | } 57 | 58 | if (strlen($bytes) !== 16) { 59 | throw new InvalidArgument('$bytes must be a 16-byte octet string'); 60 | } 61 | 62 | $bytes = $this->binary->applyVersionAndVariant($bytes, Version::Custom); 63 | 64 | return new UuidV8($bytes); 65 | } 66 | 67 | /** 68 | * @throws InvalidArgument 69 | */ 70 | public function createFromBytes(string $identifier): UuidV8 71 | { 72 | /** @var UuidV8 */ 73 | return $this->createFromBytesInternal($identifier); 74 | } 75 | 76 | /** 77 | * @throws InvalidArgument 78 | */ 79 | public function createFromHexadecimal(string $identifier): UuidV8 80 | { 81 | /** @var UuidV8 */ 82 | return $this->createFromHexadecimalInternal($identifier); 83 | } 84 | 85 | /** 86 | * @throws InvalidArgument 87 | */ 88 | public function createFromInteger(int | string $identifier): UuidV8 89 | { 90 | /** @var UuidV8 */ 91 | return $this->createFromIntegerInternal($identifier); 92 | } 93 | 94 | /** 95 | * @throws InvalidArgument 96 | */ 97 | public function createFromString(string $identifier): UuidV8 98 | { 99 | /** @var UuidV8 */ 100 | return $this->createFromStringInternal($identifier); 101 | } 102 | 103 | protected function getVersion(): Version 104 | { 105 | return Version::Custom; 106 | } 107 | 108 | protected function getUuidClass(): string 109 | { 110 | return UuidV8::class; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Uuid/Variant.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | /** 20 | * The variant number describes the layout of the UUID. 21 | * 22 | * | **Msb0** | **Msb1** | **Msb2** | **Msb3** | **Variant** | **Description** | 23 | * | :------: | :------: | :------: | :------: | :---------: | :---------------------------------------------------------- | 24 | * | 0 | x | x | x | 1-7 | Reserved, NCS backward compatibility, and includes Nil UUID | 25 | * | 1 | 0 | x | x | 8-9, A-B | The variant specified in this document | 26 | * | 1 | 1 | 0 | x | C-D | Reserved, Microsoft Corporation backward compatibility | 27 | * | 1 | 1 | 1 | x | E-F | Reserved for future definition, and includes Max UUID | 28 | * 29 | * In reading this table, we find that, if the first 3 bits of the variant field are all 1's (i.e., the decimal value 7), 30 | * then the variant is reserved for future definition. If the first three bits are two 1's followed by a 0 (i.e., the 31 | * decimal value 6), then the variant is reserved for Microsoft Corporation. If the first two bits are a 1 and 0 (i.e., 32 | * the decimal value 2), then the variant is for RFC 9562. Finally, if the first bit is 0, then it's reserved for NCS, 33 | * for backward compatibility. 34 | * 35 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-4.1 RFC 9562, section 4.1. Variant Field. 36 | */ 37 | enum Variant: int 38 | { 39 | /** 40 | * Reserved. Network Computing System (NCS) backward compatibility, and includes Nil UUID as per 41 | * {@link https://www.rfc-editor.org/rfc/rfc9562#section-5.9 RFC 9562, section 5.9}. 42 | */ 43 | case Ncs = 0b0; 44 | 45 | /** 46 | * The variant specified in {@link https://www.rfc-editor.org/rfc/rfc9562 RFC 9562}. 47 | */ 48 | case Rfc = 0b10; 49 | 50 | /** 51 | * Reserved. Microsoft Corporation backward compatibility. 52 | */ 53 | case Microsoft = 0b110; 54 | 55 | /** 56 | * Reserved for future definition and includes Max UUID as per 57 | * {@link https://www.rfc-editor.org/rfc/rfc9562#section-5.10 RFC 9562, section 5.10}. 58 | */ 59 | case Future = 0b111; 60 | 61 | /** 62 | * The variant specified in {@link https://www.rfc-editor.org/rfc/rfc9562 RFC 9562} and previously in 63 | * {@link https://www.rfc-editor.org/rfc/rfc4122 RFC 4122}. 64 | * 65 | * An alias for {@see self::Rfc}. 66 | */ 67 | public const Rfc4122 = self::Rfc; // phpcs:ignore 68 | 69 | /** 70 | * The variant specified in {@link https://www.rfc-editor.org/rfc/rfc9562 RFC 9562}. 71 | * 72 | * An alias for {@see self::Rfc}. 73 | */ 74 | public const Rfc9562 = self::Rfc; // phpcs:ignore 75 | } 76 | -------------------------------------------------------------------------------- /src/Uuid/Version.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier\Uuid; 18 | 19 | /** 20 | * The version number describes how the UUID was generated. 21 | * 22 | * | **Msb0** | **Msb1** | **Msb2** | **Msb3** | **Version** | **Description** | 23 | * | :------: | :------: | :------: | :------: | :---------: | :---------------------------------------------------------- | 24 | * | 0 | 0 | 0 | 0 | 0 | Unused | 25 | * | 0 | 0 | 0 | 1 | 1 | The Gregorian time-based UUID | 26 | * | 0 | 0 | 1 | 0 | 2 | Reserved for DCE Security version | 27 | * | 0 | 0 | 1 | 1 | 3 | The name-based version that uses MD5 hashing | 28 | * | 0 | 1 | 0 | 0 | 4 | The randomly or pseudo-randomly generated version | 29 | * | 0 | 1 | 0 | 1 | 5 | The name-based version that uses SHA-1 hashing | 30 | * | 0 | 1 | 1 | 0 | 6 | Reordered Gregorian time-based UUID | 31 | * | 0 | 1 | 1 | 1 | 7 | Unix Epoch time-based UUID | 32 | * | 1 | 0 | 0 | 0 | 8 | Reserved for custom UUID formats | 33 | * | 1 | 0 | 0 | 1 | 9 | Reserved for future definition | 34 | * | 1 | 0 | 1 | 0 | 10 | Reserved for future definition | 35 | * | 1 | 0 | 1 | 1 | 11 | Reserved for future definition | 36 | * | 1 | 1 | 0 | 0 | 12 | Reserved for future definition | 37 | * | 1 | 1 | 0 | 1 | 13 | Reserved for future definition | 38 | * | 1 | 1 | 1 | 0 | 14 | Reserved for future definition | 39 | * | 1 | 1 | 1 | 1 | 15 | Reserved for future definition | 40 | * 41 | * @link https://www.rfc-editor.org/rfc/rfc9562#section-4.2 RFC 9562, section 4.2. Verson Field. 42 | */ 43 | enum Version: int 44 | { 45 | /** 46 | * The Gregorian time-based UUID specified in RFC 9562. 47 | */ 48 | case GregorianTime = 0b0001; 49 | 50 | /** 51 | * Reserved for the DCE Security version, with embedded POSIX UUIDs. 52 | */ 53 | case DceSecurity = 0b0010; 54 | 55 | /** 56 | * The name-based version specified in RFC 9562 that uses MD5 hashing. 57 | */ 58 | case NameMd5 = 0b0011; 59 | 60 | /** 61 | * The randomly or pseudorandomly generated version specified in RFC 9562. 62 | */ 63 | case Random = 0b0100; 64 | 65 | /** 66 | * The name-based version specified in RFC 9562 that uses SHA-1 hashing. 67 | */ 68 | case NameSha1 = 0b0101; 69 | 70 | /** 71 | * Reordered Gregorian time-based UUID specified in RFC 9562. 72 | */ 73 | case ReorderedGregorianTime = 0b0110; 74 | 75 | /** 76 | * Unix Epoch time-based UUID specified in RFC 9562. 77 | */ 78 | case UnixTime = 0b0111; 79 | 80 | /** 81 | * Reserved for custom format UUID formats specified in RFC 9562. 82 | */ 83 | case Custom = 0b1000; 84 | 85 | /** 86 | * Alias for {@see self::GregorianTime} 87 | */ 88 | public const V1 = self::GregorianTime; 89 | 90 | /** 91 | * Alias for {@see self::DceSecurity} 92 | */ 93 | public const V2 = self::DceSecurity; 94 | 95 | /** 96 | * Alias for {@see self::NameMd5} 97 | */ 98 | public const V3 = self::NameMd5; 99 | 100 | /** 101 | * Alias for {@see self::Random} 102 | */ 103 | public const V4 = self::Random; 104 | 105 | /** 106 | * Alias for {@see self::NameSha1} 107 | */ 108 | public const V5 = self::NameSha1; 109 | 110 | /** 111 | * Alias for {@see self::ReorderedGregorianTime} 112 | */ 113 | public const V6 = self::ReorderedGregorianTime; 114 | 115 | /** 116 | * Alias for {@see self::UnixTime} 117 | */ 118 | public const V7 = self::UnixTime; 119 | 120 | /** 121 | * Alias for {@see self::Custom} 122 | */ 123 | public const V8 = self::Custom; 124 | } 125 | -------------------------------------------------------------------------------- /src/UuidFactory.php: -------------------------------------------------------------------------------- 1 | 12 | * @license https://opensource.org/licenses/MIT MIT License 13 | */ 14 | 15 | declare(strict_types=1); 16 | 17 | namespace Ramsey\Identifier; 18 | 19 | use Identifier\BytesIdentifierFactory; 20 | use Identifier\IntegerIdentifierFactory; 21 | use Identifier\StringIdentifierFactory; 22 | use Ramsey\Identifier\Exception\InvalidArgument; 23 | 24 | /** 25 | * A factory for creating UUIDs. 26 | */ 27 | interface UuidFactory extends 28 | BytesIdentifierFactory, 29 | IntegerIdentifierFactory, 30 | StringIdentifierFactory 31 | { 32 | public function create(): Uuid; 33 | 34 | public function createFromBytes(string $identifier): Uuid; 35 | 36 | /** 37 | * Creates a new instance of a UUID from the given hexadecimal representation. 38 | * 39 | * @throws InvalidArgument MUST throw if the identifier is not a legal value. 40 | */ 41 | public function createFromHexadecimal(string $identifier): Uuid; 42 | 43 | public function createFromInteger(int | string $identifier): Uuid; 44 | 45 | public function createFromString(string $identifier): Uuid; 46 | } 47 | --------------------------------------------------------------------------------