diff --git a/docs/concepts.md b/docs/concepts.md index 073038885..97ab1612b 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -24,12 +24,24 @@ on the standard Config class if nothing is found in the database. ## User Providers -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`. +You can use your own models to handle user persistence. Shield calls this the "User Provider" class. +A default model is provided for you by the `CodeIgniter\Shield\Models\UserModel` class. You can change +this in the `Config\Auth::$userProvider` setting. The only requirement is that your new class +MUST extend the provided `UserModel`. + +Shield has a CLI command to quickly create a custom `UserModel` class by running the following +command in the terminal: + +```console +php spark shield:model UserModel +``` + +The class name is optional. If none is provided, the generated class name would be `UserModel`. + +You should set `Config\Auth::$userProvider` as follows: ```php -public string $userProvider = UserModel::class; +public string $userProvider = \App\Models\UserModel::class; ``` ## User Identities diff --git a/src/Commands/Generators/UserModelGenerator.php b/src/Commands/Generators/UserModelGenerator.php new file mode 100644 index 000000000..fdb76596c --- /dev/null +++ b/src/Commands/Generators/UserModelGenerator.php @@ -0,0 +1,92 @@ +] [options]'; + + /** + * @var array + */ + protected $arguments = [ + 'name' => 'The model class name. If not provided, this will default to `UserModel`.', + ]; + + /** + * @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 the 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->setHasClassName(false); + + $class = $params[0] ?? CLI::getSegment(2) ?? 'UserModel'; + + if (! $this->verifyChosenModelClassName($class, $params)) { + CLI::error('Cannot use `ShieldUserModel` as class name as this conflicts with the parent class.', 'light_gray', 'red'); + + return; // @TODO when CI4 is at v4.3+, change this to `return 1;` to signify failing exit + } + + $params[0] = $class; + + $this->execute($params); + } + + /** + * The chosen class name should not conflict with the alias of the parent class. + */ + private function verifyChosenModelClassName(string $class, array $params): bool + { + helper('inflector'); + + if (array_key_exists('suffix', $params) && ! strripos($class, 'Model')) { + $class .= 'Model'; + } + + return strtolower(pascalize($class)) !== 'shieldusermodel'; + } +} diff --git a/src/Commands/Generators/Views/usermodel.tpl.php b/src/Commands/Generators/Views/usermodel.tpl.php new file mode 100644 index 000000000..8937a2978 --- /dev/null +++ b/src/Commands/Generators/Views/usermodel.tpl.php @@ -0,0 +1,19 @@ +<@php + +declare(strict_types=1); + +namespace {namespace}; + +use CodeIgniter\Shield\Models\UserModel as ShieldUserModel; + +class {class} extends ShieldUserModel +{ + protected function initialize(): void + { + $this->allowedFields = [ + ...$this->allowedFields, + + // 'first_name', + ]; + } +} diff --git a/src/Config/Registrar.php b/src/Config/Registrar.php index f05263c9c..b22ec0a30 100644 --- a/src/Config/Registrar.php +++ b/src/Config/Registrar.php @@ -49,4 +49,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..cf009e373 --- /dev/null +++ b/tests/Commands/UserModelGeneratorTest.php @@ -0,0 +1,142 @@ +streamFilter = stream_filter_append(STDOUT, 'CITestStreamFilter'); + $this->streamFilter = stream_filter_append(STDERR, 'CITestStreamFilter'); + + if (is_file(HOMEPATH . 'src/Models/UserModel.php')) { + copy(HOMEPATH . 'src/Models/UserModel.php', HOMEPATH . 'src/Models/UserModel.php.bak'); + } + + $this->deleteTestFiles(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + stream_filter_remove($this->streamFilter); + $this->deleteTestFiles(); + + if (is_file(HOMEPATH . 'src/Models/UserModel.php.bak')) { + copy(HOMEPATH . 'src/Models/UserModel.php.bak', HOMEPATH . 'src/Models/UserModel.php'); + unlink(HOMEPATH . 'src/Models/UserModel.php.bak'); + } + } + + private function deleteTestFiles(): void + { + $possibleFiles = [ + APPPATH . 'Models/UserModel.php', + HOMEPATH . 'src/Models/UserModel.php', + ]; + + foreach ($possibleFiles as $file) { + clearstatcache(true, $file); + + if (is_file($file)) { + unlink($file); + } + } + } + + private function getFileContents(string $filepath): string + { + return (string) @file_get_contents($filepath); + } + + public function testGenerateUserModel(): void + { + command('shield:model UserModel'); + + $filepath = APPPATH . 'Models/UserModel.php'; + $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer); + $this->assertFileExists($filepath); + + $contents = $this->getFileContents($filepath); + $this->assertStringContainsString('namespace App\Models;', $contents); + $this->assertStringContainsString('class UserModel extends ShieldUserModel', $contents); + $this->assertStringContainsString('use CodeIgniter\Shield\Models\UserModel as ShieldUserModel;', $contents); + $this->assertStringContainsString('protected function initialize(): void', $contents); + } + + public function testGenerateUserModelCustomNamespace(): void + { + command('shield:model UserModel --namespace CodeIgniter\\\\Shield'); + + $filepath = HOMEPATH . 'src/Models/UserModel.php'; + $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer); + $this->assertFileExists($filepath); + + $contents = $this->getFileContents($filepath); + $this->assertStringContainsString('namespace CodeIgniter\Shield\Models;', $contents); + $this->assertStringContainsString('class UserModel extends ShieldUserModel', $contents); + $this->assertStringContainsString('use CodeIgniter\Shield\Models\UserModel as ShieldUserModel;', $contents); + $this->assertStringContainsString('protected function initialize(): void', $contents); + } + + public function testGenerateUserModelWithForce(): void + { + command('shield:model UserModel'); + command('shield:model UserModel --force'); + + $this->assertStringContainsString('File overwritten: ', CITestStreamFilter::$buffer); + $this->assertFileExists(APPPATH . 'Models/UserModel.php'); + } + + public function testGenerateUserModelWithSuffix(): void + { + command('shield:model User --suffix'); + + $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer); + + $filepath = APPPATH . 'Models/UserModel.php'; + $this->assertFileExists($filepath); + $this->assertStringContainsString('class UserModel extends ShieldUserModel', $this->getFileContents($filepath)); + } + + public function testGenerateUserModelWithoutClassNameInput(): void + { + command('shield:model'); + + $this->assertStringContainsString('File created: ', CITestStreamFilter::$buffer); + + $filepath = APPPATH . 'Models/UserModel.php'; + $this->assertFileExists($filepath); + $this->assertStringContainsString('class UserModel extends ShieldUserModel', $this->getFileContents($filepath)); + } + + public function testGenerateUserCannotAcceptShieldUserModelAsInput(): void + { + command('shield:model ShieldUserModel'); + + $this->assertStringContainsString('Cannot use `ShieldUserModel` as class name as this conflicts with the parent class.', CITestStreamFilter::$buffer); + $this->assertFileDoesNotExist(APPPATH . 'Models/UserModel.php'); + + CITestStreamFilter::$buffer = ''; + + command('shield:model ShieldUser --suffix'); + + $this->assertStringContainsString('Cannot use `ShieldUserModel` as class name as this conflicts with the parent class.', CITestStreamFilter::$buffer); + $this->assertFileDoesNotExist(APPPATH . 'Models/UserModel.php'); + } +}