When it comes to building RESTful APIs, PHP’s open source Laravel framework is a leading choice. This is one of the reasons why Laravel remains a top 5 backend framework for web development. Laravel also makes testing a breeze by providing an easy-to-use testing suite to help you test your API endpoints. In this post, we will build a token-based authentication API with Laravel, write tests for our endpoints, and automate the build and testing process with CircleCI.
Prerequisites
To follow along with this post, you will need a few things:
- PHP >= 7.1 installed on your system (you can confirm that your version in high enough by running the command
php -v
on your terminal) - Composer installed globally (confirm this by running the
composer
command on your terminal) - Git installed on your system
- A GitHub account
- A CircleCI account
Once you have these up and running, you will be ready to follow along with the tutorial.
Scaffolding the Laravel API
We will be scaffolding a Laravel 5.6 application. This version of Laravel is supported by PHP >= 7.1 which is the minimum PHP version specified for the tutorial. First, create a new Laravel project by running the following command on your terminal:
composer create-project --prefer-dist laravel/laravel my-laravel-api "5.6.*"
This will create a project with the name my-laravel-api
in the location where the command was run. Feel free to give your project your preferred name. Once the scaffolding process is completed, it’s time to initialize the project as a Git repository and link it up with GitHub. Go into your project’s root and run the following command:
git init
Then run the following commands to make your first commit.
git add .
git commit -m “Initial Commit”
Next, create a GitHub repository for your project and push your code to it. I have created a repository with the same name as my project on my GitHub account and will be pushing using the following commands:
git remote add origin https://github.com/coderonfleek/my-laravel-api.git
git push -u origin master
These commands will register the remote repository and push the code to its master branch. Once this process is completed, you will have your local repository linked to your remote repository on GitHub.
Setting up the database
For this tutorial, SQLite will be used for our testing and main databases. Oftentimes, the main database is a more sophisticated DBMS like MySQL or MSSQL, but we will keep things in this post simple, for the sake of demonstration. We will keep the configuration for our main database in our .env
file, and then create a .env.testing
file to hold our test database configuration.
You already have the .env
file which comes with the default project scaffolding. Create a new file named .env.testing
and copy the contents of .env.example
(which is also created by default) into it.
Replace the database configuration section of both files (.env
and .env.testing
) with the following configuration:
DB_CONNECTION=sqlite
DB_HOST=null
DB_PORT=null
DB_DATABASE=database/database.sqlite
DB_USERNAME=null
DB_PASSWORD=null
We are using the same configuration for both files because we are using SQLite databases for both our main and testing databases. In the above code snippet, we have set our connection to sqlite
and our database to database.sqlite
which is contained in our database
folder. We have yet to create this database.sqlite
file, so go into the database
folder at the root of your project and create this file.
The final task in setting up our database is to use one of Laravel’s helpers to point to our SQLite database file in our database configuration file in config/database.php
. Inside the config/database.php
file, replace the SQLite configuration in the connections
array with the one below:
'sqlite' => [
'driver' => 'sqlite',
'database' => database_path('database.sqlite'),
'prefix' => '',
]
This will ensure that our SQLite configuration always points to the correct database path if we ever change the environment in which our application is running.
To confirm that all is working perfectly, let’s migrate our database by running the following command:
php artisan migrate
A successful migration should display something similar to the screenshot below:
Make sure that you ignore the database file by declaring it in your .gitignore
file.
Setting up token-based authentication with Passport
Our next task is to set up our API project to use token-based authentication. We will be doing that using the Laravel Passport OAuth library which provides a full OAuth2 server implementation for your Laravel application. First, let’s install the laravel/passport
package:
composer require laravel/passport v7.5.1
Once the installation is done, run the following command to run the migrations that come with laravel/passport
:
php artisan migrate
This should print a screen similar to the one below on your command line:
Passport requires encryption keys to generate access tokens, these keys need to be generated and saved in the database. To generate these keys, run the following command:
php artisan passport:install
After running this command successfully, you should see your client secrets generated.
The next step is to add the Laravel\Passport\HasApiTokens
trait to our App\User
model. This will introduce helper methods from the laravel/passport
package into the application to help with inspecting a user’s token and scopes. Open the app/User.php
file and replace its contents with the code below:
<?php
namespace App;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Laravel\Passport\HasApiTokens;
class User extends Authenticatable
{
use Notifiable, HasApiTokens;
protected $fillable = [
'name', 'email', 'password',
];
protected $hidden = [
'password', 'remember_token',
];
}
In the above file, we imported the Laravel\Passport\HasApiTokens
trait and instructed our class to use it.
Next, you need to call the Passport::routes
method inside the boot method of your AuthServiceProvider
contained inside the app/Providers/AuthServiceProvider.php
file. Open this file and replace the contents with the code below:
<?php
namespace App\Providers;
use Laravel\Passport\Passport;
use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
'App\Model' => 'App\Policies\ModelPolicy',
];
public function boot()
{
$this->registerPolicies();
Passport::routes();
}
}
The final configuration that Passport requires is setting it as the driver
option in the API authentication guard inside config/auth.php
.
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'passport',
'provider' => 'users',
],
],
With this, the application will now use Passport’s TokenGuard to authenticate incoming requests.
Creating the API endpoints
We are going to be building a simple user profile API. The user will be able to do the following:
- Sign up for a new account
- Log in to their accounts using their login credentials
- Fetch their profile
- Log out of the application
We need to create API endpoints for these four tasks. Go into the file routes/api.php
and replace its contents with the code below:
<?php
use Illuminate\Http\Request;
Route::group([
'prefix' => 'auth'
], function () {
Route::post('login', 'AuthController@login');
Route::post('signup', 'AuthController@signup');
Route::group([
'middleware' => 'auth:api'
], function() {
Route::get('logout', 'AuthController@logout');
Route::get('user', 'AuthController@user');
});
});
In the above code, we create an auth
route group and create four routes for the four tasks we stated earlier.
The auth/logout
and auth/user
routes are authenticated with the auth:api
middleware to ensure that only authenticated access is allowed to these endpoints. We also referenced AuthController
above which is the controller that we will be creating next. Before that, delete the app/Http/Controllers/Auth
folder that comes by default with the project scaffolding. Then, run the following command to create the controller:
php artisan make:controller AuthController
Now place the following code into the newly created controller:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Carbon\Carbon;
use App\User;
class AuthController extends Controller
{
/**
* Create user
*
* @param [string] name
* @param [string] email
* @param [string] password
* @param [string] password_confirmation
* @return [string] message
*/
public function signup(Request $request)
{
$request->validate([
'name' => 'required|string',
'email' => 'required|string|email|unique:users',
'password' => 'required|string|confirmed'
]);
$user = new User([
'name' => $request->name,
'email' => $request->email,
'password' => bcrypt($request->password)
]);
$user->save();
return response()->json([
'message' => 'Successfully created user!'
], 201);
}
/**
* Login user and create token
*
* @param [string] email
* @param [string] password
* @param [boolean] remember_me
* @return [string] access_token
* @return [string] token_type
* @return [string] expires_at
*/
public function login(Request $request)
{
$request->validate([
'email' => 'required|string|email',
'password' => 'required|string',
'remember_me' => 'boolean'
]);
$credentials = request(['email', 'password']);
if(!Auth::attempt($credentials)){
return response()->json([
'message' => 'Unauthorized'
], 401);
}
$user = $request->user();
$tokenResult = $user->createToken('Personal Access Token');
$token = $tokenResult->token;
if ($request->remember_me){
$token->expires_at = Carbon::now()->addWeeks(1);
}
$token->save();
return response()->json([
'access_token' => $tokenResult->accessToken,
'token_type' => 'Bearer',
'expires_at' => Carbon::parse(
$tokenResult->token->expires_at
)->toDateTimeString()
]);
}
/**
* Logout user (Revoke the token)
*
* @return [string] message
*/
public function logout(Request $request)
{
$request->user()->token()->revoke();
return response()->json([
'message' => 'Successfully logged out'
]);
}
/**
* Get the authenticated User
*
* @return [json] user object
*/
public function user(Request $request)
{
return response()->json($request->user());
}
}
Whoa! That is a lot of code. Let’s go through the code method by method.
The signup method
This method receives an email, a password, and the user’s name as parameters, it then creates a user using the Lucid model’s save()
method on the User
model and returns a success message along with a status of 201.
The login method
This method receives the user email and password and an optional remember_me
parameter. It then verifies the credentials, and upon successful verification, returns the access token, token type, and the time that the token will expire.
The logout method
This method receives and revokes the access token given to the user then sends a success message upon log out.
The user method
This method receives the user’s access token and uses it to return the user’s details.
Note: If you see a file .rnd
created in your project, ignore this file by adding it to your .gitignore
file.i>
Testing the API endpoints
It’s now time to write some tests for our API endpoints. Let’s create a test by running the following command:
php artisan make:test UserTest
This creates the UserTest
test file inside the tests/Feature
folder. Delete the ExampleTest.php
file that comes by default in the folder. Open the UserTest.php
file just created and replace its contents with the code below:
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Foundation\Testing\RefreshDatabase;
use App\User;
class UserTest extends TestCase
{
use WithFaker;
private $password = "mypassword";
public function testUserCreation()
{
$name = $this->faker->name();
$email = $this->faker->email();
$response = $this->postJson('/api/auth/signup', [
'name' => $name,
'email' => $email,
'password' => $this->password,
'password_confirmation' => $this->password
]);
$response
->assertStatus(201)
->assertExactJson([
'message' => "Successfully created user!",
]);
}//testUserCreation
}
The file above contains a single test that tests our /api/auth/signup
API endpoint to ensure that users are able to sign up successfully. The test generates a random email address and user’s name using the already-installed PHP Faker library along with a generic password to create a new user account. The test asserts an HTTP status of 201 (created) and the response message as defined in our controller.
Running tests locally
Now to take our test for a spin. We will be using PHPUnit to run our test. An installation of PHPUnit comes with the phpunit
CLI command. You can have this command running globally with a global installation of PHPUnit, or have it at project level. The default scaffolded Laravel project already has the package installed locally which enables us to run our tests.
To run the tests, run the following command at the project root:
./vendor/bin/phpunit
This runs the phpunit command using our local installation of the package. If everything goes fine, all your tests should run successfully and you should have a screen similar to the one below.
Automating our Tests with CircleCI
Time to introduce the power of CI/CD into our Laravel API. We will be creating a pipeline that ensures that once we push new code, our tests automatically run and we get either a successful or failed CI/CD pipeline status. We are going to be using CircleCI to achieve this. Our pipeline will consist of the following steps to enable us to achieve our aim:
- Spinning up the required environment
- Installing dependencies with composer
- Setting up a
.env
environment file for the project - Setting up our test database and running our migrations
- Generating encryption keys for the
laravel/passport
package - Running our tests
To create a CI/CD pipeline, we need to write a configuration file that CircleCI will use for setting up the pipeline. Go into the root of your project and create a folder named .circleci
. Inside this folder, create a file named config.yml
.
Spinning up the required environment
In this step, we will be pulling the circleci/php:7.4-node-browsers
Docker image from the CircleCI image registry. This image contains PHP 7.4 installed alongside Node and browsers. We then update our environment and install the necessary packages.
Begin the configuration file with the code below:
version: 2
jobs:
build:
docker:
# Specify the version you desire here
- image: circleci/php:7.4-node-browsers
steps:
- checkout
- run:
name: "Prepare Environment"
command: |
sudo apt update
sudo docker-php-ext-install zip
Installing dependencies with Composer
The next step is to checkout our code from our repository into the environment that has been set up and install our project dependencies. We also cache our dependencies to increase the speed of our pipeline on subsequent runs.
# Download and cache dependencies
- restore_cache:
keys:
# "composer.lock" can be used if it is committed to the repo
- v1-dependencies-{{ checksum "composer.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run:
name: "Install Dependencies"
command: composer install -n --prefer-dist
- save_cache:
key: v1-dependencies-{{ checksum "composer.json" }}
paths:
- ./vendor
Setting up a .env environment file for the project
Our Laravel project requires an environment file to run, the .env
file. This file is a sensitive file which is why it is ignored in our .gitignore
file. This is the reason why we created another version of the file .env.testing
to store the credentials we need for running our tests. Unlike .env, .env.testing is pushed to our repo as it does not contain sensitive information.
We will now rename our .env.testing
to .env
and generate an application key for the Laravel application.
- run:
name: "Create Environment file and generate app key"
command: |
mv .env.testing .env
php artisan key:generate
Setting up our test database and running our migrations
The next task is to set up the test database and run our migrations, do that by adding the following step:
- run:
name: "Create database and run migration"
command: |
touch database/database.sqlite
php artisan migrate --env=testing
Generating encryption keys for the laravel/passport package
Next, we generate the encryption keys needed by Passport to generate access tokens.
- run:
name: "Generate Passport encryption keys"
command: php artisan passport:install
Running our tests
Finally, with our environment and project fully set up, we can now run our tests using the local installation of the phpunit
package.
- run:
name: "Run Tests"
command: ./vendor/bin/phpunit
Full config
Awesome. Now we have our configuration file setup. Below is the full config.yml
file:
# PHP CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-php/ for more details
#
version: 2
jobs:
build:
docker:
# Specify the version you desire here
- image: circleci/php:7.4-node-browsers
steps:
- checkout
- run:
name: "Prepare Environment"
command: |
sudo apt update
sudo docker-php-ext-install zip
# Download and cache dependencies
- restore_cache:
keys:
# "composer.lock" can be used if it is committed to the repo
- v1-dependencies-{{ checksum "composer.json" }}
# fallback to using the latest cache if no exact match is found
- v1-dependencies-
- run:
name: "Install Dependencies"
command: composer install -n --prefer-dist
- save_cache:
key: v1-dependencies-{{ checksum "composer.json" }}
paths:
- ./vendor
# prepare the database
- run:
name: "Create Environment file and generate app key"
command: |
mv .env.testing .env
php artisan key:generate
- run:
name: "Create database and run migration"
command: |
touch database/database.sqlite
php artisan migrate --env=testing
- run:
name: "Generate Passport encryption keys"
command: php artisan passport:install
# run tests with phpunit
- run:
name: "Run Tests"
command: ./vendor/bin/phpunit
Commit all your changes and push to the project’s remote repository.
Connecting the API project to CircleCI
Now its time to hand over this configuration to CircleCI to set up our pipeline. We do that by connecting our project to CircleCI. Head over to your CircleCI dashboard and add the project in the Add Project section.
Next to your project, click Set Up Project. This will bring you to a page similar to the one below.
Click Start building to begin building your project. CircleCI will begin to run your pipeline configuration. If you have followed the instructions correctly, you should have a successful build indicated by the screen below.
Click into this workflow and scroll down to the Run Tests section to see the results of your test.
Perfect. We have been able to successfully power our Laravel API with a CI/CD pipeline that automatically runs our test using CircleCI.
Let’s add one more test to our UserTest.php
file to test the /api/auth/login
endpoint and push our code to observe how CircleCI automatically runs our newly added test. Add the following test below the testUserCreation
test.
public function testUserLogin()
{
$name = $this->faker->name();
$email = $this->faker->email();
$user = new User([
'name' => $name,
'email' => $email,
'password' => bcrypt($this->password)
]);
$user->save();
$response = $this->postJson('/api/auth/login', [
'email' => $email,
'password' => $this->password
]);
$response->assertStatus(200);
$this->assertAuthenticated();
}
Now save the file, commit it, and push your changes to GitHub. CircleCI will once again run the pipeline with the tests, including the one we just added.
Conclusion
In this article, we developed a token-based authentication API with Laravel, tested it, and automated the building and testing process using a CI/CD pipeline created for and run by CircleCI. This is just the starting point for bringing automation into the development of our Laravel APIs. There is a lot more we can do with CircleCI to create a more robust and complex pipeline that takes away a lot of the manual tasks involved in application development.
I hope you have learned something valuable from this post. Happy Coding :)
Fikayo is a fullstack developer and author with over a decade of experience developing web and mobile solutions. He is currently the Software Lead at Tech Specialist Consulting and develops courses for Packt and Udemy. He has a strong passion for teaching and hopes to become a full-time author.