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.
Task Description from Client
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.
Generating Artisan Command
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.
Validate Parameters
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:
No Automated Tests?
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 :)
GitHub commit for this lesson:
Links to read/watch more:
- Creating Artisan commands: Official Docs
- Database Transactions: Official Docs or Tutorial: Database Transactions in Laravel - 5 Open-Source Examples or Video: Laravel DB Transactions - Example When/How to Use Them
Question should we add the time zone data in the public function handle() as well? If the client lives in one time zone and the tours are in another time zone shouldn’t this be apparent to the user?
My reason for wanting to include the time zone in hear; is in Tennessee we have two 2 time zones; Central, and Eastern. Nashville TN is in the Central time zone and Chattanooga TN is in the Eastern time zone they are just 2 hours apart however if you were to leave Nashville going to say the Chattanooga Aquarium that closes at 6:00 pm Eastern time., and you left at say 3:30 to 4:00 Central Time expecting to get there be for they clouded, you may not make it; If you don’t figure in the time difference as well as your driving time. You see the time at the Aquarium will be 7:00 Pm or there abuts.
I have a frontend template that has images and video’s, more than one per tour; how can we combined these in hear, so that when the tour data is searched the photos can come up as well. Is this posable using this API?
Can we use some thing like this to set the timezone for the user and for the tour?
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');
}
Yes, probably, but there will be huge amount of options, then. For the list of the timezones, you may read this course lesson.
Wouldn’t it be better to create an admin user, and client user; using table seeder at the very beginning?
Maybe. But the client requested differently.