diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27a4a66 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +/build +/coverage +/vendor +/.idea +/.vscode +.DS_Store +.phpunit.result.cache +.php_cs.cache +_lighthouse_ide_helper.php +composer.lock +phpunit.xml +testbench.yaml diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..7a75270 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,216 @@ + true, + 'array_indentation' => true, + 'array_push' => true, + 'backtick_to_shell_exec' => true, + 'binary_operator_spaces' => [ + 'default' => 'single_space', + 'operators' => [ + '=>' => 'align', + ], + ], + 'clean_namespace' => true, + 'combine_consecutive_issets' => true, + 'combine_consecutive_unsets' => true, + 'combine_nested_dirname' => true, + 'comment_to_phpdoc' => true, + 'compact_nullable_typehint' => true, + 'dir_constant' => true, + 'doctrine_annotation_braces' => true, + 'doctrine_annotation_indentation' => true, + 'echo_tag_syntax' => [ + 'format' => 'short', + ], + 'ereg_to_preg' => true, + 'explicit_indirect_variable' => true, + 'explicit_string_variable' => true, + 'full_opening_tag' => true, + 'function_declaration' => true, + 'function_to_constant' => true, + 'function_typehint_space' => true, + 'heredoc_indentation' => true, + 'list_syntax' => true, + 'method_chaining_indentation' => true, + 'modernize_types_casting' => true, + 'multiline_comment_opening_closing' => true, + 'native_function_type_declaration_casing' => true, + 'no_break_comment' => true, + 'no_null_property_initialization' => true, + 'no_php4_constructor' => true, + 'no_superfluous_phpdoc_tags' => true, + 'no_unneeded_curly_braces' => true, + 'no_unneeded_final_method' => true, + 'no_unset_cast' => true, + 'no_unset_on_property' => true, + 'no_useless_else' => false, + 'no_useless_sprintf' => true, + 'non_printable_character' => true, + 'nullable_type_declaration_for_default_null_value' => true, + 'operator_linebreak' => true, + 'ordered_class_elements' => [ + 'order'=> [ + 'use_trait', + 'constant_public', + 'constant_protected', + 'constant_private', + 'property_public', + 'property_protected', + 'property_private', + 'construct', + 'destruct', + 'magic', + 'phpunit', + 'method', + ], + ], + 'ordered_interfaces' => true, + 'ordered_traits' => true, + 'php_unit_construct' => true, + 'php_unit_expectation' => true, + 'php_unit_fqcn_annotation' => true, + 'php_unit_method_casing' => ['case' => 'snake_case'], + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_align' => true, + 'phpdoc_no_alias_tag' => true, + 'phpdoc_order' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_separation' => true, + 'phpdoc_tag_type' => true, + 'phpdoc_trim_consecutive_blank_line_separation' => true, + 'phpdoc_types_order' => true, + 'phpdoc_var_annotation_correct_order' => true, + 'return_assignment' => true, + 'self_static_accessor' => true, + 'simple_to_complex_string_variable' => true, + // 'simplified_if_return' => true, + 'simplified_null_return' => true, + 'single_space_after_construct' => true, + 'standardize_increment' => true, + 'ternary_to_null_coalescing' => true, + 'types_spaces' => true, + 'array_syntax' => ['syntax' => 'short'], + 'blank_line_after_namespace' => true, + 'blank_line_after_opening_tag' => true, + 'blank_line_before_statement' => ['statements' => ['return']], + 'braces' => true, + 'cast_spaces' => true, + 'class_attributes_separation' => true, + 'class_definition' => true, + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => true, + 'elseif' => true, + 'encoding' => true, + 'full_opening_tag' => true, + 'fully_qualified_strict_types' => true, // added by Shift + 'function_declaration' => true, + 'function_typehint_space' => true, + 'heredoc_to_nowdoc' => true, + 'include' => true, + 'increment_style' => ['style' => 'post'], + 'indentation_type' => true, + 'linebreak_after_opening_tag' => true, + 'line_ending' => true, + 'lowercase_cast' => true, + 'constant_case' => true, + 'lowercase_keywords' => true, + 'lowercase_static_reference' => true, // added from Symfony + 'magic_method_casing' => true, // added from Symfony + 'magic_constant_casing' => true, + 'method_argument_space' => true, + 'native_function_casing' => true, + 'no_alias_functions' => true, + 'no_extra_blank_lines' => ['tokens' => ['extra', 'throw', 'use', 'use_trait']], + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_closing_tag' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_mixed_echo_print' => ['use' => 'echo'], + 'no_multiline_whitespace_around_double_arrow' => true, + 'multiline_whitespace_before_semicolons' => ['strategy' => 'no_multi_line'], + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_after_function_name' => true, + 'no_spaces_around_offset' => true, + 'no_spaces_inside_parenthesis' => true, + 'no_trailing_comma_in_list_call' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'no_unused_imports' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unreachable_default_argument_value' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'normalize_index_brace' => true, + 'not_operator_with_successor_space' => true, + 'object_operator_without_whitespace' => true, + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'phpdoc_indent' => true, + 'phpdoc_inline_tag_normalizer' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_package' => true, + 'phpdoc_no_useless_inheritdoc' => true, + 'phpdoc_scalar' => true, + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_summary' => true, + 'phpdoc_to_comment' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_var_without_name' => true, + 'self_accessor' => true, + 'short_scalar_cast' => true, + 'simplified_null_return' => true, + 'single_blank_line_at_eof' => true, + 'single_blank_line_before_namespace' => true, + 'single_class_element_per_statement' => true, + 'single_import_per_statement' => true, + 'single_line_after_imports' => true, + 'single_line_comment_style' => ['comment_types' => ['hash']], + 'single_quote' => true, + 'space_after_semicolon' => true, + 'standardize_not_equals' => true, + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'ternary_operator_spaces' => true, + 'trailing_comma_in_multiline' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'use_arrow_functions' => true, + 'visibility_required' => ['elements' => ['method', 'property']], + 'whitespace_after_comma_in_array' => true, + 'return_type_declaration' => ['space_before' => 'none'], + 'psr_autoloading' => true, +]; + +$finder = Symfony\Component\Finder\Finder::create() + ->notPath('bootstrap/*') + ->notPath('storage/*') + ->notPath('resources/view/mail/*') + ->notPath('vendor') + ->in([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->notName('index.php') + ->notName('server.php') + ->notName('_ide_helper.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +$config = new PhpCsFixer\Config(); + +return $config + ->setRiskyAllowed(true) + ->setFinder($finder) + ->setRules($rules) + ->setUsingCache(false) + ->setCacheFile(__DIR__ . '.php_cs.cache'); diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..36af26c --- /dev/null +++ b/composer.json @@ -0,0 +1,47 @@ +{ + "name": "malico/laravel-nanoid", + "license": "MIT", + "authors": [ + { + "name": "Malico", + "email": "hi@malico.me" + } + ], + "autoload": { + "psr-4": { + "Malico\\LaravelNanoid\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/" + } + }, + "require": { + "hidehalo/nanoid-php": "^1.1" + }, + "require-dev": { + "ext-pdo": "*", + "pestphp/pest": "^1.20", + "friendsofphp/php-cs-fixer": "^3.1", + "orchestra/testbench": "^6.21" + }, + "extra": { + "laravel": { + "providers": [ + "Malico\\LaravelNanoid\\LaravelNanoidServiceProvider" + ] + } + }, + "archive": { + "exclude": [ + "tests/", + "*.xml" + ] + }, + "scripts": { + "post-autoload-dump": [ + "@php ./vendor/bin/testbench package:discover --ansi" + ] + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..772a86c --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,16 @@ + + + + + ./src + + + + + ./tests + + + + + + \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..14bebca --- /dev/null +++ b/readme.md @@ -0,0 +1,140 @@ +# Laravel Nanoid + +## Introduction + +A simple drop-in solution for providing nanoid support for the IDs of your Eloquent models. (Stripe-like IDs) + +## Installing + +`composer require malico/laravel-nanoid` + +## Usage + +There are two ways to use this package: + +- By extending the provided model classes (preferred and simplest method). +- By using the provided model trait (allows for extending another model class). + +### Extend the model classes + +While creating your model, you can extend the `Eloquent Model` class provided by the package. + +```php +{$model->getKeyName()} = $model->generateNanoid(); + }); + } +} +``` + +Take note of the `$incrementing` and `$keyType` properties. Also make sure within your `boot` method you call the `parent::boot` method and then add the `creating` event listener. +Also make sure your id column is set to `string` type. + +```php +// migration file +public function up() +{ + Schema::create('test_models', function (Blueprint $table) { + $table->string('id')->primary(); + // + $table->timestamps(); + }); +} +``` + +To create a new migration, use the artisan command `make:nanoid-migration`. All arguments work the same as the `make:migration` command. + +## Options + +1. Prefix: To Specify a Prefix for the IDs, you can specify a prefix by add `nanoPrefix' property to your model class. +2. Same applies for the length of the ID. + +```php +creator = $creator; + $this->composer = $composer; + } +} diff --git a/src/Console/Commands/NanoidModelMakeCommand.php b/src/Console/Commands/NanoidModelMakeCommand.php new file mode 100644 index 0000000..a7cc7f2 --- /dev/null +++ b/src/Console/Commands/NanoidModelMakeCommand.php @@ -0,0 +1,81 @@ + $option[0] !== 'pivot' + ); + } + + /** + * Create a migration file for the model. + * + * @return void + */ + protected function createMigration() + { + $table = Str::snake(Str::pluralStudly(class_basename($this->argument('name')))); + + $this->call(NanoidMigrateMakeCommand::class, [ + 'name' => "create_{$table}_table", + '--create' => $table, + ]); + } + + /** + * Get the value of a command option. + * + * @param null|string $key + * + * @return null|array|bool|string + */ + public function option($key = null) + { + if ($key === 'pivot') { + return false; + } + + return parent::option($key); + } +} diff --git a/src/Database/Migrations/MigrationCreator.php b/src/Database/Migrations/MigrationCreator.php new file mode 100644 index 0000000..b0a9df6 --- /dev/null +++ b/src/Database/Migrations/MigrationCreator.php @@ -0,0 +1,19 @@ +string('id')->primary(); + // + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('{{ table }}'); + } +} diff --git a/src/Database/Migrations/stubs/migration.stub b/src/Database/Migrations/stubs/migration.stub new file mode 100755 index 0000000..fd0e437 --- /dev/null +++ b/src/Database/Migrations/stubs/migration.stub @@ -0,0 +1,28 @@ +{$model->getKeyName()} = $model->generateNanoid(); + }); + } +} diff --git a/src/Eloquent/NanoidTrait.php b/src/Eloquent/NanoidTrait.php new file mode 100644 index 0000000..690742c --- /dev/null +++ b/src/Eloquent/NanoidTrait.php @@ -0,0 +1,44 @@ +nanoidLength)) { + return random_int($this->nanoidLength[0], $this->nanoidLength[1]); + } + + return $this->nanoidLength; + } + + /** + * Generate a nanoid. + */ + public function generateNanoid(): string + { + $client = new Client(); + + return $this->nanoidPrefix . $client->generateId($this->getNanoidLength(), Client::MODE_DYNAMIC); + } +} diff --git a/src/Eloquent/stubs/nanoid.model.stub b/src/Eloquent/stubs/nanoid.model.stub new file mode 100644 index 0000000..3a99299 --- /dev/null +++ b/src/Eloquent/stubs/nanoid.model.stub @@ -0,0 +1,11 @@ +app->runningInConsole()) { + $this->commands([ + NanoidModelMakeCommand::class, + NanoidMigrateMakeCommand::class, + ]); + } + } +} diff --git a/testbench.yaml.dist b/testbench.yaml.dist new file mode 100644 index 0000000..de12a80 --- /dev/null +++ b/testbench.yaml.dist @@ -0,0 +1,3 @@ +env: +providers: + - Malico\LaravelNanoid\LaravelNanoidServiceProvider diff --git a/tests/Console/Commands/NanoidMigrateMakeCommandTest.php b/tests/Console/Commands/NanoidMigrateMakeCommandTest.php new file mode 100644 index 0000000..478eeb6 --- /dev/null +++ b/tests/Console/Commands/NanoidMigrateMakeCommandTest.php @@ -0,0 +1,16 @@ +each(function ($file) { + unlink($file); + }); +}); + +test('it generates a migration', function () { + $this->artisan(NanoidMigrateMakeCommand::class, ['name' => 'create_test_models_table'])->assertExitCode(0); + + $this->assertFileExists(glob(database_path('migrations/*_create_test_models_table.php'))[0]); + $this->assertStringContainsString('$table->string(\'id\')->primary();', file_get_contents(glob(database_path('migrations/*_create_test_models_table.php'))[0])); +}); diff --git a/tests/Console/Commands/NanoidModelMakeCommandTest.php b/tests/Console/Commands/NanoidModelMakeCommandTest.php new file mode 100644 index 0000000..4b64ee6 --- /dev/null +++ b/tests/Console/Commands/NanoidModelMakeCommandTest.php @@ -0,0 +1,29 @@ +each(function ($file) { + unlink($file); + }); + + if (file_exists(app_path('Models/TestModel.php'))) { + unlink(app_path('Models/TestModel.php')); + } +}); + +test('it generates a model', function () { + $this->artisan(NanoidModelMakeCommand::class, ['name' => 'TestModel'])->assertExitCode(0); + + $this->assertFileExists(app_path('Models/TestModel.php')); + $this->assertStringContainsString('use Malico\LaravelNanoid\Eloquent\Model;', file_get_contents(app_path('Models/TestModel.php'))); + $this->assertStringContainsString('extends Model', file_get_contents(app_path('Models/TestModel.php'))); +}); + +test('it generates a migration if specified', function () { + $this->artisan(NanoidModelMakeCommand::class, ['name' => 'TestModel', '--migration' => true])->assertExitCode(0); + + $this->assertFileExists( + glob(database_path('migrations/*_create_test_models_table.php'))[0] + ); +}); diff --git a/tests/Eloquent/ModelTest.php b/tests/Eloquent/ModelTest.php new file mode 100644 index 0000000..041abbc --- /dev/null +++ b/tests/Eloquent/ModelTest.php @@ -0,0 +1,41 @@ +getKey())->toBeString(); +}); + +test('creates nanoid with prefix before saving', function () { + $model = BasicModelWithPrefix::create(); + + expect(Str::is('pl_*', $model->getKey()))->toBeTrue(); +}); + +test('creates nanoid with length before saving', function () { + $model = BasicModelWithLength::create(); + + expect(Str::length($model->getKey()))->toBe(3); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..d47845c --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,5 @@ +in(__DIR__); diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..4cb4adb --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,45 @@ +setUpDatabase(); + } + + protected function setUpDatabase(): void + { + $this->app['db'] + ->connection('testing') + ->getSchemaBuilder() + ->create('test_migrations_with_string_id', function (Blueprint $table): void { + $table->string('id')->primary(); + $table->timestamps(); + }); + } + + /** + * Get package providers. + * + * @param \Illuminate\Foundation\Application $app + * + * @return array + */ + protected function getPackageProviders($app) + { + return [ + LaravelNanoidServiceProvider::class, + ]; + } +}