Twitter From Scratch Using Laravel: Custom Authentication

In our previous lesson we've learned about testing in Laravel. In this lesson we will be talking about custom authentication. We will be going to customize the Laravel's built-in authentication, because remember in our previous lesson we've added a
username
column in our users
table. So, expected that our registration will no longer work at this point.First, let's tweak our
users
table. Let's add first_name
and last_name
column and remove name
column so that in the future it will be more easy if we only want to display the first or the last name:
...
$table->string('first_name');
$table->string('last_name');
...
Next, let's update our user model factory and add the following:
...
'first_name' => $faker->firstName,
'last_name' => $faker->lastName,
...
Next, let's make a feature test in
tests/Feature/UserTest.php
that will make sure a user can register:
...
/** @test */
public function a_user_can_register()
{
$email = 'johndoe@gmail.com';
$this->post('/register', [
'first_name' => 'John',
'last_name' => 'Doe',
'email' => $email,
'password' => 'secret',
'password_confirmation' => 'secret'
]);
$this->assertDatabaseHas('users', [
'email' => $email
]);
}
...
As you can see, we're sending a POST request to the server using http://twitter.dev/register
url and passing an array of data, then we assert that the users
table has a johndoe@gmail.com
email.Let's run our test:
phpunit
...
There was 1 failure:
1) Tests\Feature\UserTest::a_user_can_register
Failed asserting that a row in the table [users] matches the attributes {
"email": "johndoe@gmail.com"
}.
The table is empty.
...
As expected it failed, but notice that our test is not telling us why it failed.Let's fix that by throwing an exception directly when we are in testing environment. Let's update our
render
method in app/Exceptions/Handler.php
:
...
public function render($request, Exception $exception)
{
if (app()->environment() == 'testing') throw $exception;
return parent::render($request, $exception);
}
...
Now, when we run our test again we should get a more specific error:
phpunit
...
There was 1 error:
1) Tests\Feature\UserTest::a_user_can_register
Illuminate\Validation\ValidationException: The given data failed to pass validation.
/home/vagrant/Projects/twitter/vendor/laravel/framework/src/Illuminate/Validation/Validator.php:291
/home/vagrant/Projects/twitter/vendor/laravel/framework/src/Illuminate/Foundation/Auth/RegistersUsers.php:31
...
So, we now have an idea it is validation related issue.Notice that in line 31 of that
RegistersUsers
trait it calls a $this->validator()
method which can be found in app/Http/Controllers/Auth/RegisterController.php
:
...
protected function validator(array $data)
{
return Validator::make($data, [
'first_name' => 'required|max:255',
'last_name' => 'required|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|min:6|confirmed',
]);
}
...
So, basically we just need to remove the name
key-value pair and add a first_name
and last_name
key-value pair into the array.Let's run our test again:
phpunit
...
1) Tests\Feature\UserTest::a_user_can_register
ErrorException: Undefined index: name
/home/vagrant/Projects/twitter/app/Http/Controllers/Auth/RegisterController.php:67
...
Now, we get another error.Again, same with the
validator()
method we just have add and remove some key-value pair in the array in the create()
method:
...
protected function create(array $data)
{
return User::create([
'first_name' => $data['first_name'],
'last_name' => $data['last_name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
]);
}
...
Run the test again:
phpunit
...
There was 1 error:
1) Tests\Feature\UserTest::a_user_can_register
Illuminate\Database\QueryException: SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: users.username (SQL: insert into "users" ("email","password", "updated_at", "created_at") values (johndoe@gmail.com, $2y$10$i0NVVXj7PpxMvkykyQb1eO1DIjFXk9k.0MoPItIc3lItCx2zeyqxS, 2017-04-03 06:49:52, 2017-04-03 06:49:52))
...
This is because we are not passing any value in the username
column which is not nullable.Now, we have two options here. One is to be explicit, meaning we have to add a username field in our registration form, or to be implicit meaning the system will generate a username based on the user's first and last name and let the user update it later. In my opinion the less fields in our registration form the better so let's do the latter.
First, let's import a package called Eloquent-Sluggable. This package will automatically do the job for us:
composer require cviebrock/eloquent-sluggable
Next, let's add it into our
providers
array in config/app.php
file:
'providers' => [
// ...
Cviebrock\EloquentSluggable\ServiceProvider::class,
];
Next, let's run:
php artisan vendor:publish --provider="Cviebrock\EloquentSluggable\ServiceProvider"
This will create a sluggable.php
file under the config
directory that contains Eloquent-Sluggable's default configuration.Next, let's update our
User
model:
//...
use Cviebrock\EloquentSluggable\Sluggable;
class User extends Authenticatable
{
use Sluggable;
//...
/**
* Return the sluggable configuration array for this model.
*
* @return array
*/
public function sluggable()
{
return [
'username' => [
'source' => ['first_name', 'last_name']
]
];
}
//...
}
Notice in the sluggable
method we just return an array which corresponds to our columns name.Let's run our test again:
phpunit
There was 1 error:
1) Tests\Feature\UserTest::a_user_can_register
Illuminate\Database\QueryException: SQLSTATE[23000]: Integrity constraint violation: 19 NOT NULL constraint failed: users.first_name (SQL: insert into "users" ("email", "password", "username", "updated_at", "created_at") values (johndoe@gmail.com, $2y$10$5X3nmjki7lC.Ep1SaOx81uy3wZ0NqWv1hk.1meBHwyiozAFzCK6Ni, , 2017-04-03 07:41:52, 2017-04-03 07:41:52))
Now, we have an error on first_name
field. This is because Laravel is protecting us from mass-assignment vulnerability.We can fix that by adding our
first_name
and last_name
column into the $fillable
property in our User
model:
//...
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'first_name', 'last_name', 'email', 'password',
];
//..
Run our test again:
phpunit
OK (7 tests, 7 assertions)
and we get green. Good job!Next, let's update our registration form in
resources/views/auth/register.blade.php
and add the following:
<div class="form-group{{ $errors->has('first_name') ? ' has-error' : '' }}">
<label for="name" class="col-md-4 control-label">First name</label>
<div class="col-md-6">
<input id="name" type="text" class="form-control" name="first_name" value="{{ old('first_name') }}" autofocus>
@if ($errors->has('first_name'))
<span class="help-block">
<strong>{{ $errors->first('first_name') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group{{ $errors->has('last_name') ? ' has-error' : '' }}">
<label for="name" class="col-md-4 control-label">Last name</label>
<div class="col-md-6">
<input id="name" type="text" class="form-control" name="last_name" value="{{ old('last_name') }}" autofocus>
@if ($errors->has('last_name'))
<span class="help-block">
<strong>{{ $errors->first('last_name') }}</strong>
</span>
@endif
</div>
</div>
Our registration form should now look like this.Lastly, let's add an assertion in our
a_user_can_register
test that a user is logged in after the registration using $response->assertRedirect('/home');
because we know that only logged in users can access that /home
page:
/** @test */
public function a_user_can_register()
{
$email = 'johndoe@gmail.com';
$response = $this->post('/register', [
'first_name' => 'John',
'last_name' => 'Doe',
'email' => $email,
'password' => 'secret',
'password_confirmation' => 'secret'
]);
$this->assertDatabaseHas('users', [
'email' => $email
]);
$response->assertRedirect('/home');
}
When we run our test again we should get green.We have now a fully functional registration and login feature in our website. As always make sure to run
php artisan migrate:refresh --seed
before you test it in the browser.That's all for this lesson. If you have any questions please don't hesitate to write it in the comment below and see you in the next lesson. Thanks!
View the source code for this lesson on GitHub.
Previous: Our First TestNext: Email Verification with Test
Post a Comment