How to build a RESTful API in Slim 3 - Part 2: Creating application endpoints
You will need Composer, git and PHP 7+ set up on your local machine. Basic knowledge of PHP will be helpful.
In the previous tutorial, we had a brief introduction to slim3 and we set up our application to use Laravel’s Eloquent to communicate with our database. We also created our database schema and setup Phinx to help with our migration files. Finally, we included a validation package that ensures our users submit the right data to our API.
In this tutorial, we will proceed to build our controller and models. We will also create endpoints and test the output data using Postman.
Prerequisites
- You have read the first part of this guide
- Have Postman installed on your local machine
Make an endpoint that creates offers and vouchers
Before we proceed we need to create our controller and models. We will use one controller to handle all our endpoints, while our models handle all interactions with the database. Open your terminal and run these commands to create the following files:
$ touch app/Controllers/VoucherController.php
$ mkdir app/Models
$ touch app/Models/User.php
$ touch app/Models/Offer.php
$ touch app/Models/Voucher.php
$ mkdir app/Helpers
$ touch app/Helpers/Validator.php
To create our first endpoint, we will need to accept multiple email addresses from the user. These email addresses need to be validated. Open the app/Helpers/Validator.php
file and edit as follows:
// app/Helpers/Validator.php
<?php
namespace App\Helpers;
use Respect\Validation\Validator as Respect;
class Validator extends Respect {
public static function validateEmails($email_list)
{
if(!is_array($email_list)) return false;
foreach ($email_list as $email) {
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
return false;
}
}
return true;
}
}
In the above code, we used the Respect
validator and filtered through all emails being sent to our API. If the email field is empty, we want to validation error to the user.
Next, open the app/Controllers/VoucherController.php
file and edit as follows:
// app/Controllers/VoucherController.php
<?php
namespace App\Controllers;
use App\Models\Offer;
use App\Models\User;
use App\Models\Voucher;
use App\Helpers\Validator;
use Psr\Http\Message\{
ServerRequestInterface as Request,
ResponseInterface as Response
};
class VoucherController extends Controller
{
}
Awesome! Now that we have our controller and models set up properly, we need to tackle our first task.
Task: For a given Special Offer and an expiration date, we need to generate for each Recipient a Voucher Code
To solve this, open your routes file and replace with the content below. Your routes file is located here routes/web.php
// routes/web.php
<?php
use App\Controllers\VoucherController;
$app->post('/api/offers/create', VoucherController::class . ':createOffers');
The endpoint we just created points to a createOffers
method in our VoucherController file. Let us create the method in our VoucherController
file. Open the file and edit as follows:
// app/Controllers/VoucherController.php
[...]
public function createOffers(Request $request, Response $response, $args)
{
// checks to ensure we have valid inputs
$validator = $this->c->validator->validate($request, [
'email_list' => Validator::arrayType(),
'expires_at' => Validator::date()->notBlank(),
'name' => Validator::alnum("'-_")->notBlank(),
'discount' => Validator::intVal()->noWhitespace()->notBlank(),
]);
if ($validator->isValid()) {
$offer_model = new Offer();
$voucher_model = new Voucher();
$user_model = new User();
// Create new offer
$created_offer = $offer_model->create($request);
if ($created_offer) {
// get id of users from the email, if email does not exist, create the user and return users_id
$get_user_user_ids = $user_model->findMultipleEmail($request->getParam('email_list'));
$voucher_codes = $voucher_model->create($created_offer->id, $get_user_user_ids );
}
return $response->withStatus(201)->withJson([
'status' => 'Success',
'offer_details' => $created_offer,
'voucher_details' => $voucher_codes,
'message' => $created_offer ? 'Offer Created' : 'Error Creating Offer'
]);
} else {
// return an error on failed validation, with a statusCode of 400
return $response->withStatus(400)->withJson([
'status' => 'Validation Error',
'message' => $validator->getErrors()
]);
}
}
[...]
You will notice we connected to three other models, offer
, voucher
and user.
The offer
model redirects to a create
method that receives our $request
object. Remember, our $request
object contains the input submitted on our /offers/create
endpoint. Now, let us create the create
method in our offer
model. Open the app/models/offer
and edit as follows:
// app/models/Offer.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Offer extends Model
{
use SoftDeletes;
protected $fillable = [
'name', 'discount', 'expires_at'
];
public function create($request)
{
$created_offer = self::firstOrCreate([
'name' => $request->getParam('name'),
'discount' => $request->getParam('discount'),
'expires_at' => $request->getParam('expires_at')
]);
return $created_offer;
}
}
Now that we have created our offer
, we need to create voucher codes for all recipients. Remember, our recipient information is part of the input fields to be submitted to the /offers/create
endpoint. Before vouchers can be issued to our users, we need to create the users. You will notice a method in our create
method from our VoucherController
that redirects to the findMultipleEmail()
method on our user
model.
Next, open the /app/models/user
model and insert the following content to create the findMultipleEmail()
method :
// app/models/User.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class User extends Model
{
use SoftDeletes;
protected $fillable = [
'name', 'email'
];
public static function findMultipleEmail($email_list)
{
// gets id of existing user, if user does not exist, create new user and return users id
$users_id = [];
foreach ($email_list as $email) {
$user_details = static::firstOrCreate(['email' =>$email]);
array_push($users_id, $user_details->id);
}
return $users_id;
}
}
We are using a firstOrCreate()
Eloquent method because we might receive a request to create vouchers for users that already exist in our database and that of users that do not exist. With firstOrCreate(['email' =>$email_list])
, it checks if the user exists. If they do, it returns the user’s details, if it does not, it creates a new user.
The last piece to this puzzle is, creating the vouchers and assigning them to the users created. From our create
method in the VoucherController
, you will notice we have a create()
method that links to our voucher model and it accepts two arguments, the offer_id
and users_id
.
Now, open the /app/models/voucher
model and insert the following content to create the create()
method :
// app/models/Voucher.php
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Voucher extends Model
{
use SoftDeletes;
protected $fillable = [
'code',
];
public function create($offer_id, $users_id)
{
// Generate 8 random hex code for voucher
foreach ($users_id as $key => $user_id) {
$vouchers['voucher'][$key]['code'] = substr(md5(rand()), 0, 8);
$vouchers['voucher'][$key]['offer_id'] = $offer_id;
$vouchers['voucher'][$key]['user_id'] = $user_id;
}
// insert into the database
self::insert($vouchers['voucher']);
return $vouchers;
}
}
With this, we are done creating our first endpoint 💃🏼.
Run this command on your terminal to serve our app
$ php -S localhost:9000 -t public
Using Postman, make a POST request to this endpoint http://localhost:9000/api/offers/create
endpoint. Navigate to the Body section on the tab and pass the following as parameters:
name:Childrens day Special
discount:25
expires_at:2018-8-25 23:50:49
email_list[0]:hello@gmail.com
email_list[1]:hey@gmail.com
email_list[2]:holla@gmail.com
Your output should look like this:
Make an endpoint that validates a voucher code and email
Task: We need to provide an endpoint, which will receive a voucher code and email and validates the voucher code. In case it is valid, return the percentage discount and set the date of usage
To solve this, we will create a new method in our VoucherController called validateVoucher()
. This method will receive as input from the user, voucher_code
and email
. Once we receive these details, we will check our database to ensure that the email address exists. If the email address exists, we will proceed to check if the voucher code belongs to the user.
If that passes validation, then we will get the percentage discount on the offer, mark the voucher as used and store the date of usage. If our validation fails, we will send an error message as output to the user.
First, we need to update our routes. Open your routes file and edit as follows. Your routes file is located here routes/web.php
// routes/web.php
[..]
$app->post('/offers/create', VoucherController::class . ':createOffers');
$app->post('/api/voucher/validate', VoucherController::class . ':validateVoucher');
Open the VoucherController and edit as follows:
// app/Controllers/VoucherController.php
[...]
} else {
//return an error on failed validation, with a statusCode of 400
return $response->withStatus(400)->withJson([
'status' => 'Error',
'message' => $validator->getErrors()
]);
}
}
public function validateVoucher(Request $request, Response $response, $args)
{
$validator = $this->c->validator->validate($request, [
'voucher' => Validator::alnum()->notBlank(),
'email' => Validator::email()->noWhitespace()->notBlank(),
]);
if ($validator->isValid()) {
$voucher = $request->getParam('voucher');
$email = $request->getParam('email');
$voucher_model = new Voucher();
$user_model = new User();
// check if user exist
$user_details = $user_model->findEmail($email);
if ($user_details) {
// Assertain that the voucher code belongs to the user and has not expired/not yet used
$validate_voucher = $voucher_model->validateVoucher($voucher, $user_details->id);
if (!$validate_voucher->isEmpty()) {
// activate and set date voucher was used
$activate_voucher = $voucher_model->activateVoucher($voucher, $user_details->id);
// return voucher details
return $response->withStatus(200)->withJson([
'status' => (bool) $validate_voucher,
'count' => count($validate_voucher),
'data' => $validate_voucher,
'message' => count($validate_voucher) >= 1 ? 'Success': 'No Voucher found'
]);
} else {
// return failure message if voucher does not exist
return $response->withStatus(403)->withJson([
'status' => 'Error',
'message' => 'Voucher details is invalid'
]);
}
} else {
// return failure message if user does not exist
return $response->withStatus(400)->withJson([
'status' => 'Error',
'message' => 'User does not exist'
]);
}
} else {
// return failure message if validation fails
return $response->withStatus(400)->withJson([
'status' => 'Validation Error',
'message' => $validator->getErrors()
]);
}
}
[...]
We used a findEmail()
which receives $email
as an argument and connects to the user model. The method goes to the database to check if the user exists. If the user exist, it will return the user’s details back to the controller.
Open the user
model and edit as follows:
// app/Models/User.php
[...]
return $users_id;
}
public static function findEmail($email)
{
return static::where('email', $email)->first();
}
}
We also have a method called validateVoucher()
that receives two parameters, voucher_code
and user_id
. The goes into the voucher model and checks that the voucher exist, and it also checks to ensure that the voucher belongs to the user requesting for it.
Finally, we called activateVoucher()
method which activates the voucher, sets the status as used and stores the date in which it was used.
Open the voucher
model and edit as follows:
// app/Models/Voucher.php
[...]
return $vouchers;
}
// Assertain that the voucher code belongs to the user and has not expired/not yet used
public function validateVoucher($voucher, $user_id)
{
$voucher_details = self::leftjoin('users', 'vouchers.user_id', '=', 'users.id')
->leftjoin('offers', 'vouchers.offer_id', '=', 'offers.id')
->select('vouchers.code', 'users.id as user_id', 'users.email', 'offers.expires_at','offers.name as offer_name','offers.discount as percentage_discount')
->where([
['vouchers.code', $voucher],
['vouchers.user_id', $user_id],
['vouchers.is_used', 0],
['offers.expires_at', '>', \Carbon\Carbon::now()],
])
->get();
return ($voucher_details == null ? [] : $voucher_details);
}
// activate voucher code, set is_used and date_used fields
public function activateVoucher($voucher, $user_id)
{
$activate_voucher = self::where([
['code', $voucher],
['user_id', $user_id],
])
->update(array('is_used' => 1, 'date_used' => \Carbon\Carbon::now() ));
return $activate_voucher;
}
[...]
With this, we are done creating our second endpoint 💃🏼.
Using Postman, make a POST request to http://localhost:9000/api/voucher/validate
endpoint.
Navigate to the Body section on the tab and pass the following as parameters:
voucher:INSERT-VOUCHER-CODE-HERE
email:hello@gmail.com
Your output should look like this:
Make an endpoint that fetches all valid voucher codes for a user
For any given email , return all valid voucher codes with the names of the user and the name of the special offer
To achieve this, we will create a new method in our VoucherController called fetchAllValidVoucherPerUser()
. This method will receive as email
as input from the user. Once we have the users email, we will check our database to ensure that the email address exists. If the email address exists, we will proceed to retrieve all the valid voucher codes of the user.
Keep in mind that what qualifies as valid voucher codes are:
- Voucher code is yet to be used.
- The offer has not expired
First, we need to update our routes. Open your routes file and edit as follows. Your routes file is located here routes/web.php
// routes/web.php
[..]
$app->post('/api/offers/create', VoucherController::class . ':createOffers');
$app->post('/api/voucher/validate', VoucherController::class . ':validateVoucher');
$app->get('/api/voucher/list', VoucherController::class . ':fetchAllValidVoucherPerUser');
Open the VoucherController and edit as follows:
// app/Controllers/VoucherController.php
[...]
} else {
return $response->withStatus(400)->withJson([
'status' => 'Validation Error!',
'message' => $validator->getErrors()
]);
}
}
public function fetchAllValidVoucherPerUser(Request $request, Response $response, $args)
{
$validator = $this->c->validator->validate($request, [
'email' => Validator::email()->noWhitespace()->notBlank(),
]);
if ($validator->isValid()) {
$email = $request->getQueryParam('email');
$voucher_model = new Voucher();
$user_model = new User();
//check if user exist
$user_details = $user_model->findEmail($email);
if ($user_details) {
//Fetch all valid user voucher codes
$users_voucher = $voucher_model->fetchSingleUserVoucher($user_details->id);
//return voucher details
return $response->withStatus(200)->withJson([
'status' => (bool) $users_voucher,
'count' => count($users_voucher),
'data' => $users_voucher
]);
} else {
//return failure message if user does not exist
return $response->withStatus(400)->withJson([
'status' => 'Error',
'message' => 'User does not exist'
]);
}
} else {
return $response->withStatus(400)->withJson([
'status' => 'Validation Error',
'message' => $validator->getErrors()
]);
}
}
[...]
Once we have the user’s email address as input, we check to ensure that the user exist using the findEmail()
method. If the user does not exists, we will return an error back to the user. If the user exists, using the fetchSingleUserVoucher()
that connects tot he voucher model, we will fetch all the valid user voucher codes.
To include the fetchSingleUserVoucher()
method, open the Voucher model and edit as follows:
// app/Models/Voucher.php
[...]
return $activate_voucher;
}
// method to fetch a single user's voucher details
public function fetchSingleUserVoucher($user_id)
{
$voucher_details = self::leftjoin('users', 'vouchers.user_id', '=', 'users.id')
->leftjoin('offers', 'vouchers.offer_id', '=', 'offers.id')
->select('vouchers.code','users.id as user_id', 'users.email', 'offers.expires_at','offers.name as offer_name','offers.discount as percentage_discount')
->where([
['vouchers.user_id', $user_id],
['vouchers.is_used', 0],
['offers.expires_at', '>', \Carbon\Carbon::now()],
])
->get();
return ($voucher_details == null ? [] : $voucher_details);
}
[...]
And that is it, we have created all the endpoints needed for our voucher pool API.
Using Postman, make a GET request to this endpoint http://localhost:9000/api/voucher/list?email=hey@gmail.com
endpoint.
Your output should look like this:
Conclusion
In this tutorial, we have looked at how to build a voucher pool API using the Slim 3 PHP framework. We set up a controller for voucher manipulation and creation. We also defined methods to fetch and create valid voucher codes. We saw how to test our output data using Postman.
The source code to the application in this article is available on GitHub.
16 October 2018
by Fisayo Afolayan