Header Ads

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);
}
...
Pro tip: If you're using Laravel Homestead make sure you comment the APP_ENV key and value under variables key in your Homestead.yaml file, or else the value of your app()->environment() will always be the value of that APP_ENV key. After that make sure to run vagrant provision.

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


No comments