The next point in the client description is about creating users. In fact, it was the very first point in the original job description, but I decided to first work with showing the data and only then with managing it by users. So now it's time.
A private (admin) endpoint to create new users. If you want, this could also be an artisan command, as you like. It will mainly be used to generate users for this exercise.
Here's something illogical: if there is only an endpoint to create users, then who/how would create the very first admin user? :)
That's why I vote for the Artisan command from the very beginning, without any API endpoint. So this is precisely what we will do in this lesson.
This part is easy. We just need to run this in Terminal:
php artisan make:command CreateUserCommand
Notice: I personally like to suffix all Terminal commands with the word "Command" at the end.
We fill in the command signature and description. And then, we need to fill in the handle()
method to create the user.
app/Console/Commands/CreateUserCommand.php:
namespace App\Console\Commands; use Illuminate\Console\Command; class CreateUserCommand extends Command{ protected $signature = 'users:create'; protected $description = 'Creates a new user'; public function handle() { // TODO }}
The question is how to ask the Artisan Command user all the data about the user: their name, email, password, and role (admin
or editor
).
Laravel provides a few ways: options and parameters. You can read about them in the official docs. I will choose to ask the Terminal user for each field value separately and collect them into a $user
array, which will be used in the User::create()
method later.
For the name
and email
fields, it's simple.
For password
, we need to hide the symbols in the Terminal, so we need to use a special $this->secret()
method.
public function handle(){ $user['name'] = $this->ask('Name of the new user'); $user['email'] = $this->ask('Email of the new user'); $user['password'] = $this->secret('Password of the new user'); // ...}
For the role
, it's more complicated because we need the user to choose the role. so we use $this->choice()
method:
public function handle(){ $user['name'] = $this->ask('Name of the new user'); $user['email'] = $this->ask('Email of the new user'); $user['password'] = $this->secret('Password of the new user'); $roleName = $this->choice('Role of the new user', ['admin', 'editor'], 1);}
To ensure that the values of admin
and editor
actually exist as Roles in the database, let's create a Seeder for this:
php artisan make:seeder RoleSeeder
database/seeders/RoleSeeder.php:
namespace Database\Seeders; use App\Models\Role;use Illuminate\Database\Seeder; class RoleSeeder extends Seeder{ public function run(): void { Role::create(['name' => 'admin']); Role::create(['name' => 'editor']); }}
And we add it to the main DatabaseSeeder
file:
database/seeders/DatabaseSeeder.php:
class DatabaseSeeder extends Seeder{ public function run(): void { $this->call(RoleSeeder::class); }}
Now we can launch the seeds:
php artisan db:seed
Or, you may want to launch only that specific class:
php artisan db:seed --class=RoleSeeder
Now, we check if the DB doesn't have the roles and return the error in that case:
use App\Models\Role; // ... public function handle(){ $user['name'] = $this->ask('Name of the new user'); $user['email'] = $this->ask('Email of the new user'); $user['password'] = $this->secret('Password of the new user'); $roleName = $this->choice('Role of the new user', ['admin', 'editor'], 1); $role = Role::where('name', $roleName)->first(); if (! $role) { $this->error('Role not found'); return -1; }}
If you want to show any error in the Artisan command Terminal output, just use $this->error()
with a string parameter.
Next, when we have those role values, we can create the User and return the success result:
use Illuminate\Support\Facades\DB;use Illuminate\Support\Facades\Hash; // ... public function handle(){ $user['name'] = $this->ask('Name of the new user'); // ... DB::transaction(function () use ($user, $role) { $user['password'] = Hash::make($user['password']); $newUser = User::create($user); $newUser->roles()->attach($role->id); });}
As you can see, we're using the Database Transaction here because we're performing multiple operations and want to make sure that if the second operation fails, the first one would be rolled back.
We also pass the $user
and $role
variables to be accessed inside that Transaction function. Also, remember to encrypt the password.
Here's the result of this command:
So, job done? Not so fast. Validation.
We asked the Artisan Command user all the fields, but we can't make sure they are valid.
So we need to validate those inputs, but how? It's not as easy as just putting $request->validate()
because, well, we don't have any $request
here.
We need to manually create a Validator, pass the validation rules to it, run it, and return the error if it fails.
use App\Models\User;use Illuminate\Support\Facades\Validator;use Illuminate\Validation\Rules\Password; // ... $validator = Validator::make($user, [ 'name' => ['required', 'string', 'max:255'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:'.User::class], 'password' => ['required', Password::defaults()],]);if ($validator->fails()) { foreach ($validator->errors()->all() as $error) { $this->error($error); } return -1;}
Now we will see validation errors like this:
In this lesson, I decided not to create Feature tests. Because this Terminal command is not actually a user-facing feature, it's a system function that would be used only by the owners of the project who has access to the server via Terminal.
Ideally, those could also be covered by tests. But I see a pretty low risk of something happening here, and the client didn't explicitly ask for it. Call me lazy? Maybe. I prefer to call it "90% quality is often good enough" :)
But you can accept the challenge and write the tests yourself as homework, and I would gladly share the link to your GitHub so others could see your talent, shoot in the comments :)