├── .github └── workflows │ ├── style.yml │ └── tests.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── config └── snowflake.php ├── phpunit.xml ├── src ├── Helpers.php ├── ServiceProvider.php ├── SnowflakeCast.php └── Snowflakes.php ├── tests └── Test.php └── tools └── .php-cs-fixer.php /.github/workflows/style.yml: -------------------------------------------------------------------------------- 1 | name: styling 2 | 3 | on: [push] 4 | 5 | jobs: 6 | style: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout code 11 | uses: actions/checkout@v2 12 | 13 | - name: Run PHP CS Fixer 14 | uses: docker://oskarstark/php-cs-fixer-ga 15 | with: 16 | args: --config=tools/.php-cs-fixer.php --allow-risky=yes 17 | 18 | - name: Extract branch name 19 | shell: bash 20 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})" 21 | id: extract_branch 22 | 23 | - name: Commit changes 24 | uses: stefanzweifel/git-auto-commit-action@v2.3.0 25 | with: 26 | commit_message: Fix styling 27 | branch: ${{ steps.extract_branch.outputs.branch }} 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | phpunit: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | os: [ubuntu-latest] 12 | php: [8.1] 13 | laravel: [8.*] 14 | dependency-version: [prefer-stable] 15 | include: 16 | - laravel: 8.* 17 | testbench: 6.* 18 | 19 | name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} 20 | 21 | steps: 22 | - name: Checkout code 23 | uses: actions/checkout@v2 24 | 25 | - name: Install SQLite 26 | run: | 27 | sudo apt-get update 28 | sudo apt-get install sqlite3 29 | 30 | - name: Setup PHP 31 | uses: shivammathur/setup-php@v2 32 | with: 33 | php-version: ${{ matrix.php }} 34 | extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv 35 | coverage: none 36 | 37 | - name: Install dependencies 38 | run: | 39 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update 40 | composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction 41 | 42 | - name: Execute tests 43 | run: vendor/bin/phpunit -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | composer.lock 3 | vendor 4 | storage 5 | tests/World/database.sqlite 6 | .DS_Store 7 | coverage 8 | .phpunit.result.cache 9 | .idea 10 | .php_cs.cache 11 | .php-cs-fixer.cache -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © Caneara and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Snowflake 2 | 3 | This package enables a Laravel application to create Twitter Snowflake identifiers. It is a very thin wrapper around the excellent [Snowflake PHP](https://github.com/godruoyi/php-snowflake) library created by Godruoyi. 4 | 5 | ## What are Snowflakes? 6 | 7 | Snowflakes are a form of unique identifier devised by Twitter. In this respect, they are similar to other unique identifier algorithms such as UUID or ULID. 8 | 9 | ## Why should I use them? 10 | 11 | Some of the benefits of using Snowflakes (over alternatives such as UUID) include: 12 | 13 | - They consist entirely of integers. 14 | - They use less space (16 characters, so it fits in a `BIGINT`). 15 | - Indexing of integers is much faster than indexing a string. 16 | - Keys begin with a timestamp, so are sortable. 17 | - Keys end with a random number, so guessing table size is not possible. 18 | - Databases handle integers more efficiently than strings. 19 | - Generation of new keys is faster (less than 1 ms). 20 | 21 | ## Installation 22 | 23 | Pull in the package using Composer: 24 | 25 | ```bash 26 | composer require caneara/snowflake 27 | ``` 28 | 29 | ## Configuration 30 | 31 | Snowflake includes a configuration file with several settings that you can use to initialize the Snowflake service. You should begin by publishing this configuration file: 32 | 33 | ```bash 34 | php artisan vendor:publish 35 | ``` 36 | 37 | ### Distributed architecture 38 | 39 | The service allows for the use of a distributed architectural setup involving data centers and worker nodes that are each responsible for generating Snowflakes according to their own designated identifiers. For maximum flexibility, as well as backward compatibility, this is the default configuration. 40 | 41 | If you do not intend to run a distributed architectural setup, then your first step should be to set the corresponding configuration value to `false`. 42 | 43 | ### Data centers and worker nodes 44 | 45 | When using a distributed architectural setup, you'll need to set the data center and worker node that the application should use when generating Snowflakes. These are both set to `1` by default, as that is a good starting point, but you are free to increase these numbers as you add more centers and nodes. 46 | 47 | The maximums for each of these configuration values is `31`. This gives you up to 31 nodes per data center, and 31 data centers in total. Therefore, you can have up `961` worker nodes each generating unique Snowflakes. 48 | 49 | > If you have disabled distributed architecture, then you can skip the data center and worker node values as they will be ignored by the service. 50 | 51 | ### Starting timestamp 52 | 53 | The service compares the Unix Epoch with the given starting timestamp as part of the process in generating a unique Snowflake. As a result, Snowflakes can be generated for up to 69 years using any given starting timestamp. 54 | 55 | In most cases, you should set this value to the current date using a format of `YYYY-MM-DD`. 56 | 57 | > Do not set the timestamp to a date in the future, as that won't achieve anything. You should also avoid using a date in the past, as that may reduce the number of years for which you can generate timestamps. 58 | 59 | ### Sequence resolver 60 | 61 | In order to handle the generation of unique keys within the same millisecond, the service uses a sequence resolver. There are several to choose from, however they each have dependencies, such as Redis. You are free to use any of them, however the default option is a good choice, as it **doesn't** have any dependencies. 62 | 63 | ## Usage 64 | 65 | You can generate a Snowflake by resolving the service out of the container and calling its `id` method: 66 | 67 | ```php 68 | resolve('snowflake')->id(); // (string) "5585066784854016" 69 | ``` 70 | 71 | > **WARNING**: Do not create instances of the Snowflake service, as doing so risks generating matching keys / introducing collisions. Instead, always resolve the Snowflake singleton out of the container. You can also use the global helper method (see below). 72 | 73 | Since this is a little cumbersome, the package also registers a global `snowflake()` helper method that you can use anywhere. 74 | 75 | ```php 76 | snowflake(); // (string) "5585066784854016" 77 | ``` 78 | 79 | ## Databases 80 | 81 | If you want to use Snowflakes in your database e.g. for primary and foreign keys, then you'll need to perform a couple of steps. 82 | 83 | First, modify your migrations so that they use the Snowflake migration methods e.g. 84 | 85 | ```php 86 | // Before 87 | $table->id(); 88 | $table->foreignId('user_id'); 89 | $table->foreignIdFor(User::class); 90 | 91 | // After 92 | $table->snowflake()->primary(); 93 | $table->foreignSnowflake('user_id'); 94 | $table->foreignSnowflakeFor(User::class); 95 | ``` 96 | 97 | Here's an example: 98 | 99 | ```php 100 | class CreatePostsTable extends Migration 101 | { 102 | public function up() 103 | { 104 | Schema::create('posts', function(Blueprint $table) { 105 | $table->snowflake()->primary(); 106 | $table->foreignSnowflake('user_id')->constrained()->cascadeOnDelete(); 107 | $table->string('title', 100); 108 | $table->timestamps(); 109 | }); 110 | } 111 | } 112 | ``` 113 | 114 | Next, if you're using Eloquent, add the package's `Snowflakes` trait to your Eloquent models: 115 | 116 | ```php 117 | SnowflakeCast::class, 145 | 'user_id' => SnowflakeCast::class, 146 | 'title' => 'string', 147 | ]; 148 | } 149 | ``` 150 | 151 | ## Contributing 152 | 153 | Thank you for considering a contribution to Snowflake. You are welcome to submit a PR containing improvements, however if they are substantial in nature, please also be sure to include a test or tests. 154 | 155 | ## License 156 | 157 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 158 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "caneara/snowflake", 3 | "description": "A package to create Twitter Snowflake identifiers", 4 | "keywords": [ 5 | "snowflake", 6 | "php", 7 | "laravel", 8 | "database" 9 | ], 10 | "type": "library", 11 | "license": "MIT", 12 | "homepage": "https://github.com/caneara/snowflake", 13 | "autoload": { 14 | "psr-4": { 15 | "Snowflake\\": "src" 16 | }, 17 | "files": [ 18 | "src/Helpers.php" 19 | ] 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "Snowflake\\Tests\\": "tests" 24 | } 25 | }, 26 | "require": { 27 | "php": "^8.0", 28 | "godruoyi/php-snowflake": "^2.0" 29 | }, 30 | "require-dev": { 31 | "orchestra/testbench": "^6.0", 32 | "phpunit/phpunit": "^9.0" 33 | }, 34 | "extra": { 35 | "laravel": { 36 | "providers": [ 37 | "Snowflake\\ServiceProvider" 38 | ] 39 | } 40 | }, 41 | "scripts": { 42 | "test": "vendor/bin/phpunit" 43 | }, 44 | "minimum-stability": "stable" 45 | } 46 | -------------------------------------------------------------------------------- /config/snowflake.php: -------------------------------------------------------------------------------- 1 | true, 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Data Center 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This value represents the data center reference that should be used by 26 | | Snowflake when generating unique identifiers. The value must be 1 - 31. 27 | | 28 | */ 29 | 30 | 'data_center' => 1, 31 | 32 | /* 33 | |-------------------------------------------------------------------------- 34 | | Worker Node 35 | |-------------------------------------------------------------------------- 36 | | 37 | | This value represents the worker node reference that should be used by 38 | | Snowflake when generating unique identifiers. The value must be 1 - 31. 39 | | 40 | */ 41 | 42 | 'worker_node' => 1, 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Start Timestamp 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This value represents the starting date for generating new timestamps. 50 | | Snowflakes can be created for 69 years past this date. In most cases, 51 | | you should set this value to the current date when building a new app. 52 | | 53 | */ 54 | 55 | 'start_timestamp' => '2022-01-01', 56 | 57 | /* 58 | |-------------------------------------------------------------------------- 59 | | Sequence Resolver 60 | |-------------------------------------------------------------------------- 61 | | 62 | | This value represents the sequencing strategy that should be used to 63 | | ensure that multiple Snowflakes generated within the same millisecond 64 | | are unique. The default is a good choice, as it has no dependencies. 65 | | 66 | */ 67 | 68 | 'sequence_resolver' => RandomSequenceResolver::class, 69 | 70 | ]; 71 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src/ 6 | 7 | 8 | 9 | 10 | tests 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Helpers.php: -------------------------------------------------------------------------------- 1 | id(); 10 | } 11 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | macros(); 20 | 21 | $this->publishes([__DIR__ . '/../config/snowflake.php' => config_path('snowflake.php')]); 22 | } 23 | 24 | /** 25 | * Register any package services. 26 | * 27 | */ 28 | public function register() : void 29 | { 30 | $this->mergeConfigFrom(__DIR__ . '/../config/snowflake.php', 'snowflake'); 31 | 32 | $this->app->singleton('snowflake', fn () => $this->singleton()); 33 | } 34 | 35 | /** 36 | * Register any custom macros. 37 | * 38 | */ 39 | protected function macros() : void 40 | { 41 | Blueprint::macro('snowflake', function($column = 'id') { 42 | return $this->unsignedBigInteger($column); 43 | }); 44 | 45 | Blueprint::macro('foreignSnowflake', function($column) { 46 | return $this->addColumnDefinition(new ForeignIdColumnDefinition($this, [ 47 | 'type' => 'bigInteger', 48 | 'name' => $column, 49 | 'autoIncrement' => false, 50 | 'unsigned' => true, 51 | ])); 52 | }); 53 | 54 | Blueprint::macro('foreignSnowflakeFor', function($model, $column = null) { 55 | return $this->foreignSnowflake($column ?: (new $model())->getForeignKey()); 56 | }); 57 | } 58 | 59 | /** 60 | * Register the Snowflake singleton service. 61 | * 62 | */ 63 | protected function singleton() : Snowflake 64 | { 65 | $distributed = config('snowflake.distributed', true); 66 | 67 | $service = new Snowflake( 68 | $distributed ? config('snowflake.data_center', 1) : null, 69 | $distributed ? config('snowflake.worker_node', 1) : null 70 | ); 71 | 72 | $timestamp = strtotime(config('snowflake.start_timestamp', '2022-01-01')) * 1000; 73 | $resolver = config('snowflake.sequence_resolver', RandomSequenceResolver::class); 74 | 75 | return $service 76 | ->setStartTimeStamp($timestamp) 77 | ->setSequenceResolver(new $resolver()); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/SnowflakeCast.php: -------------------------------------------------------------------------------- 1 | getKey()) { 15 | $model->{$model->getKeyName()} = snowflake(); 16 | } 17 | }); 18 | } 19 | 20 | /** 21 | * Disable auto-incrementing integers. 22 | * 23 | */ 24 | public function getIncrementing() : bool 25 | { 26 | return false; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/Test.php: -------------------------------------------------------------------------------- 1 | register(); 19 | } 20 | 21 | /** @test */ 22 | public function it_can_resolve_the_snowflake_service_and_generate_an_identifier() : void 23 | { 24 | $this->assertTrue(is_string(resolve('snowflake')->id())); 25 | 26 | $this->assertEquals(17, strlen(resolve('snowflake')->id())); 27 | } 28 | 29 | /** @test */ 30 | public function it_can_generate_a_snowflake_identifier_using_the_global_helper() : void 31 | { 32 | $this->assertTrue(is_string(snowflake())); 33 | 34 | $this->assertEquals(17, strlen(snowflake())); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tools/.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | notPath(dirname(__DIR__, 1) . '/bootstrap/*') 5 | ->notPath(dirname(__DIR__, 1) . '/storage/*') 6 | ->notPath(dirname(__DIR__, 1) . '/vendor') 7 | ->notPath(dirname(__DIR__, 1) . '/resources/view/mail/*') 8 | ->in([ 9 | dirname(__DIR__, 1) . '/src', 10 | dirname(__DIR__, 1) . '/tests', 11 | ]) 12 | ->name('*.php') 13 | ->notName('*.blade.php') 14 | ->ignoreDotFiles(true) 15 | ->ignoreVCS(true); 16 | 17 | return (new PhpCsFixer\Config()) 18 | ->setRules([ 19 | '@PSR2' => true, 20 | 'array_syntax' => ['syntax' => 'short'], 21 | 'ordered_imports' => ['sort_algorithm' => 'length'], 22 | 'no_unused_imports' => true, 23 | 'not_operator_with_successor_space' => true, 24 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 25 | 'phpdoc_scalar' => true, 26 | 'unary_operator_spaces' => true, 27 | 'binary_operator_spaces' => [ 28 | 'operators' => ['=' => 'align', '=>' => 'align'], 29 | ], 30 | 'blank_line_before_statement' => [ 31 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 32 | ], 33 | 'phpdoc_single_line_var_spacing' => true, 34 | 'phpdoc_var_without_name' => true, 35 | 'class_attributes_separation' => [ 36 | 'elements' => [ 37 | 'method' => 'one', 38 | 'property' => 'one', 39 | ], 40 | ], 41 | 'method_argument_space' => [ 42 | 'on_multiline' => 'ensure_fully_multiline', 43 | 'keep_multiple_spaces_after_comma' => true, 44 | ], 45 | 'method_chaining_indentation' => true, 46 | 'object_operator_without_whitespace' => true, 47 | 'no_superfluous_phpdoc_tags' => true, 48 | 'function_declaration' => [ 49 | 'closure_function_spacing' => 'none', 50 | ], 51 | ]) 52 | ->setFinder($finder); 53 | --------------------------------------------------------------------------------