diff --git a/docs/concepts.md b/docs/concepts.md
index c8cafce74..717313076 100644
--- a/docs/concepts.md
+++ b/docs/concepts.md
@@ -26,10 +26,16 @@ on the standard Config class if nothing is found in the database.
You can use your own models to handle user persistence. Shield calls this the "User Provider" class. A default model
is provided for you at `CodeIgniter\Shield\Models\UserModel`. You can change this in the `Config\Auth->userProvider` setting.
-The only requirement is that your new class MUST extend the provided `UserModel`.
+The only requirement is that your new class MUST extend the provided `UserModel`. Shield has a CLI command to quickly create a custom `MyUserModel` by running the following command in terminal:
+
+```console
+php spark shield:model MyUserModel
+```
+
+You should set `Config\Auth::$userProvider` as follows:
```php
-public $userProvider = 'CodeIgniter\Shield\Models\UserModel';
+public $userProvider = \App\Models\MyUserModel::class;
```
diff --git a/src/Commands/Generators/UserModelGenerator.php b/src/Commands/Generators/UserModelGenerator.php
new file mode 100644
index 000000000..f792abe26
--- /dev/null
+++ b/src/Commands/Generators/UserModelGenerator.php
@@ -0,0 +1,77 @@
+ [options]';
+
+ /**
+ * The Command's Arguments
+ *
+ * @var array
+ */
+ protected $arguments = [
+ 'name' => 'The model class name.',
+ ];
+
+ /**
+ * The Command's Options
+ *
+ * @var array
+ */
+ protected $options = [
+ '--namespace' => 'Set root namespace. Default: "APP_NAMESPACE".',
+ '--suffix' => 'Append the component title to the class name (e.g. User => UserModel).',
+ '--force' => 'Force overwrite existing file.',
+ ];
+
+ /**
+ * Actually execute a command.
+ */
+ public function run(array $params): void
+ {
+ $this->component = 'Model';
+ $this->directory = 'Models';
+ $this->template = 'usermodel.tpl.php';
+
+ $this->classNameLang = 'CLI.generator.className.model';
+ $this->execute($params);
+ }
+}
diff --git a/src/Commands/Generators/Views/usermodel.tpl.php b/src/Commands/Generators/Views/usermodel.tpl.php
new file mode 100644
index 000000000..e25f6cb1a
--- /dev/null
+++ b/src/Commands/Generators/Views/usermodel.tpl.php
@@ -0,0 +1,20 @@
+<@php
+
+declare(strict_types=1);
+
+namespace {namespace};
+
+use CodeIgniter\Shield\Models\UserModel;
+
+class {class} extends UserModel
+{
+ protected function initialize(): void
+ {
+ $this->allowedFields = [
+ ...$this->allowedFields,
+ // Add here your custom fields
+ // 'first_name',
+ ];
+ }
+}
+
diff --git a/src/Config/Registrar.php b/src/Config/Registrar.php
index f212e7962..0186a2fec 100644
--- a/src/Config/Registrar.php
+++ b/src/Config/Registrar.php
@@ -45,4 +45,13 @@ public static function Toolbar(): array
],
];
}
+
+ public static function Generators(): array
+ {
+ return [
+ 'views' => [
+ 'shield:model' => 'CodeIgniter\Shield\Commands\Generators\Views\usermodel.tpl.php',
+ ],
+ ];
+ }
}
diff --git a/tests/Commands/UserModelGeneratorTest.php b/tests/Commands/UserModelGeneratorTest.php
new file mode 100644
index 000000000..bb8e74b4e
--- /dev/null
+++ b/tests/Commands/UserModelGeneratorTest.php
@@ -0,0 +1,100 @@
+streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter');
+ $this->streamFilter = stream_filter_append(STDERR, 'CITestStreamFilter');
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ stream_filter_remove($this->streamFilter);
+ $result = str_replace(["\033[0;32m", "\033[0m", "\n"], '', CITestStreamFilter::$buffer);
+
+ $filepath = str_replace('APPPATH' . DIRECTORY_SEPARATOR, APPPATH, trim(substr($result, 14)));
+ if (is_file($filepath)) {
+ unlink($filepath);
+ }
+ }
+
+ protected function getFileContents(string $filepath): string
+ {
+ if (! file_exists($filepath)) {
+ return '';
+ }
+
+ return file_get_contents($filepath) ?: '';
+ }
+
+ public function testGenerateUserModel(): void
+ {
+ command('shield:model MyUserModel');
+ $filepath = APPPATH . 'Models/MyUserModel.php';
+
+ $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer);
+ $this->assertFileExists($filepath);
+
+ $this->assertStringContainsString('namespace App\Models;', $this->getFileContents($filepath));
+ $this->assertStringContainsString('class MyUserModel extends UserModel', $this->getFileContents($filepath));
+ $this->assertStringContainsString('use CodeIgniter\Shield\Models\UserModel;', $this->getFileContents($filepath));
+ $this->assertStringContainsString('protected function initialize(): void', $this->getFileContents($filepath));
+ }
+
+ public function testGenerateUserModelCustomNamespace(): void
+ {
+ command('shield:model MyUserModel --namespace CodeIgniter\\\\Shield');
+ $filepath = HOMEPATH . 'src/Models/MyUserModel.php';
+
+ $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer);
+ $this->assertFileExists($filepath);
+
+ $this->assertStringContainsString('namespace CodeIgniter\Shield\Models;', $this->getFileContents($filepath));
+ $this->assertStringContainsString('class MyUserModel extends UserModel', $this->getFileContents($filepath));
+ $this->assertStringContainsString('use CodeIgniter\Shield\Models\UserModel;', $this->getFileContents($filepath));
+ $this->assertStringContainsString('protected function initialize(): void', $this->getFileContents($filepath));
+
+ if (is_file($filepath)) {
+ unlink($filepath);
+ }
+ }
+
+ public function testGenerateUserModelWithForce(): void
+ {
+ command('shield:model MyUserModel');
+
+ command('shield:model MyUserModel --force');
+ $this->assertStringContainsString('File overwritten: ', CITestStreamFilter::$buffer);
+
+ $filepath = APPPATH . 'Models/MyUserModel.php';
+ $this->assertFileExists($filepath);
+ }
+
+ public function testGenerateUserModelWithSuffix(): void
+ {
+ command('shield:model MyUser --suffix');
+ $filepath = APPPATH . 'Models/MyUserModel.php';
+
+ $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer);
+ $this->assertFileExists($filepath);
+ $this->assertStringContainsString('class MyUserModel extends UserModel', $this->getFileContents($filepath));
+ }
+}