diff --git a/classes/local/cli/application.php b/classes/local/cli/application.php index 6db3d91..6539612 100644 --- a/classes/local/cli/application.php +++ b/classes/local/cli/application.php @@ -27,6 +27,7 @@ use local_devkit\local\cli\commands\mcp\mcp_serve; use local_devkit\local\cli\commands\plugins\plugins_list; use local_devkit\local\cli\commands\purge\handler as purge_handler; +use local_devkit\local\cli\commands\seed\users as seed_users; use Symfony\Component\Console\Application as BaseApplication; /** @@ -49,6 +50,7 @@ public function __construct() { $this->addCommand(new env_show()); $this->addCommand(new format()); $this->addCommand(new mcp_serve()); + $this->addCommand(new seed_users()); lint_handler::register($this); purge_handler::register($this); } diff --git a/classes/local/cli/commands/seed/users.php b/classes/local/cli/commands/seed/users.php new file mode 100644 index 0000000..60b1dca --- /dev/null +++ b/classes/local/cli/commands/seed/users.php @@ -0,0 +1,57 @@ +. + +declare(strict_types=1); + +namespace local_devkit\local\cli\commands\seed; + +use local_devkit\local\seeders\users as users_seeder; +use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Attribute\Option; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\ProgressIndicator; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * Command to seed a bunch of users. + * + * @package local_devkit + * @copyright 2026 Felix Yeung + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +#[AsCommand(name: 'seed:users')] +class users extends Command { + /** + * Invoke + */ + public function __invoke( + SymfonyStyle $io, + OutputInterface $output, + #[Option] + int $count = 1, + ): int { + $progress = $output instanceof ConsoleOutputInterface + ? new ProgressIndicator($output->getErrorOutput()) + : null; + $progress?->start('Starting...'); + $seeder = new users_seeder($progress, $count); + $seeder->seed(); + $progress?->finish('Completed!'); + return Command::SUCCESS; + } +} diff --git a/classes/local/seeders/base.php b/classes/local/seeders/base.php new file mode 100644 index 0000000..b4a0777 --- /dev/null +++ b/classes/local/seeders/base.php @@ -0,0 +1,44 @@ +. + +declare(strict_types=1); + +namespace local_devkit\local\seeders; + +use Symfony\Component\Console\Helper\ProgressIndicator; + +/** + * Class base + * + * @package local_devkit + * @copyright 2026 Felix + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +abstract class base { + /** + * Constructor + */ + public function __construct( + /** @var $progress */ + protected readonly ?ProgressIndicator $progress, + ) { + } + + /** + * Run the seeder. + */ + abstract public function seed(): void; +} diff --git a/classes/local/seeders/users.php b/classes/local/seeders/users.php new file mode 100644 index 0000000..ff3ecef --- /dev/null +++ b/classes/local/seeders/users.php @@ -0,0 +1,84 @@ +. + +namespace local_devkit\local\seeders; + +use csv_import_reader; +use Faker\Factory; +use InvalidArgumentException; +use Symfony\Component\Console\Helper\ProgressIndicator; +use tool_uploaduser\process; + +/** + * Class users + * + * @package local_devkit + * @copyright 2026 Felix + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class users extends base { + /** + * Constructor. + */ + public function __construct( + ?ProgressIndicator $progress, + /** @var int */ + private readonly int $count = 1, + ) { + parent::__construct($progress); + if ($count <= 0) { + throw new InvalidArgumentException('count must be positive integer'); + } + } + + #[\Override] + public function seed(): void { + global $CFG; + require_once($CFG->dirroot . '/user/lib.php'); + require_once($CFG->libdir . '/csvlib.class.php'); + + $faker = Factory::create(); + + $delimiter = ','; + $columns = ['firstname', 'lastname', 'username', 'email', 'password']; + $iid = csv_import_reader::get_new_iid('uploaduser'); + $cir = new csv_import_reader($iid, 'uploaduser'); + $cir->load_csv_content(implode($delimiter, $columns), 'utf8', $delimiter); + + $process = new process($cir, \tool_uploaduser\local\text_progress_tracker::class); + $process->set_form_data((object) ['uutype' => UU_USER_ADDNEW]); + + // Remove text_progress_tracker's output as we are using our own progress tracking. + ob_start(); + $process->process(); + ob_end_clean(); + + // This is to ensure the password passes Moodle's password policy. + $passwordsuffix = '@aA1!'; + + foreach (range(1, $this->count) as $index) { + $this->progress?->setMessage("Seeding $index/$this->count"); + $user = [ + 'firstname' => $faker->firstName(), + 'lastname' => $faker->lastName(), + 'username' => $faker->userName(), + 'email' => $faker->safeEmail(), + 'password' => $faker->password() . $passwordsuffix, + ]; + $process->process_line(array_values($user)); + } + } +} diff --git a/composer.json b/composer.json index 1a9ab58..0f0a7a3 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ "mcp/sdk": "^0.7.0", "symfony/process": "^7.4", "symfony/yaml": "^7.4", - "symfony/finder": "^7.4" + "symfony/finder": "^7.4", + "fakerphp/faker": "^1.24" }, "require-dev": { "phpstan/phpstan": "^2.1", @@ -32,4 +33,4 @@ "php": "8.3" } } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 7c34305..f111675 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "05aa70c28b53c72c92c8e6924631f81d", + "content-hash": "95edada5279d4d3225d29febe712439b", "packages": [ { "name": "doctrine/deprecations", @@ -54,6 +54,69 @@ }, "time": "2026-02-07T07:09:04+00:00" }, + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, { "name": "mcp/sdk", "version": "v0.7.0",