How to build a RESTful API in Slim 3 – Part 2: Creating application endpoints

In the previous chapter, 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 guide, we will proceed to build our controller and models. We will also create endpoints and test the output data using Postman.

Prerequisites

  1. You have read the first part of this guide
  2. 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 the 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:

Screenshot of the endpoint to create an offer

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;
        }


 // Ascertain 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:

Screenshot of the endpoint to validate vouchers

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:

  1. Voucher code is yet to be used.
  2. 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 exists using the findEmail() method. If the user does not exist, 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:

Endpoint to view all valid voucher details of a user

Conclusion

In this guide, 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.

This post was originally posted on Pusher.