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

Introduction

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:

1$ touch app/Controllers/VoucherController.php
2    $ mkdir app/Models
3    $ touch app/Models/User.php
4    $ touch app/Models/Offer.php
5    $ touch app/Models/Voucher.php
6    $ mkdir app/Helpers
7    $ 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:

1// app/Helpers/Validator.php
2    
3    <?php 
4    
5    namespace App\Helpers;
6    
7    use Respect\Validation\Validator as Respect;
8    
9    class Validator extends Respect {
10        public static function validateEmails($email_list) 
11        {
12            if(!is_array($email_list)) return false;
13            foreach ($email_list as $email) {
14                if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
15                    return false;
16                }
17            }
18            return true;
19        }
20    
21    }

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:

1// app/Controllers/VoucherController.php
2    
3    <?php
4    
5    namespace App\Controllers;
6    
7    use App\Models\Offer;
8    use App\Models\User;
9    use App\Models\Voucher;
10    use App\Helpers\Validator;
11    
12    use Psr\Http\Message\{
13        ServerRequestInterface as Request,
14        ResponseInterface as Response
15    };
16    
17    class VoucherController extends Controller
18    {
19    
20    }

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

1// routes/web.php
2    <?php
3    
4    use App\Controllers\VoucherController;
5    
6    $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:

1// app/Controllers/VoucherController.php
2    
3    [...]
4    public function createOffers(Request $request, Response $response, $args)
5        {
6            // checks to ensure we have valid inputs
7            $validator = $this->c->validator->validate($request, [
8                'email_list' => Validator::arrayType(),
9                'expires_at' => Validator::date()->notBlank(),
10                'name' => Validator::alnum("'-_")->notBlank(),
11                'discount' => Validator::intVal()->noWhitespace()->notBlank(),
12            ]);
13    
14            if ($validator->isValid()) {
15                $offer_model = new Offer();
16                $voucher_model = new Voucher();
17                $user_model = new User();
18                // Create new offer
19                $created_offer = $offer_model->create($request);
20                
21                if ($created_offer) {
22                    // get id of users from the email, if email does not exist, create the user and return users_id
23                    $get_user_user_ids  =   $user_model->findMultipleEmail($request->getParam('email_list'));
24                    $voucher_codes      =   $voucher_model->create($created_offer->id, $get_user_user_ids );
25                }    
26    
27                return $response->withStatus(201)->withJson([
28                    'status' => 'Success',
29                    'offer_details'     => $created_offer,
30                    'voucher_details'   => $voucher_codes,
31                    'message' => $created_offer ? 'Offer Created' : 'Error Creating Offer'
32                ]);
33            } else {
34                // return an error on failed validation, with a statusCode of 400
35                return $response->withStatus(400)->withJson([
36                    'status' => 'Validation Error',
37                    'message' => $validator->getErrors()
38                ]);
39            }
40        }
41    [...]

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:

1// app/models/Offer.php
2    <?php
3    
4    namespace App\Models;
5    
6    use Illuminate\Database\Eloquent\Model;
7    use Illuminate\Database\Eloquent\SoftDeletes;
8    
9    class Offer extends Model
10    {
11        use SoftDeletes;
12    
13        protected $fillable = [
14            'name', 'discount', 'expires_at'
15        ];
16    
17        public function create($request)
18        {
19            
20            $created_offer = self::firstOrCreate([
21                'name'          => $request->getParam('name'),
22                'discount'      => $request->getParam('discount'),
23                'expires_at'    => $request->getParam('expires_at')
24            ]);
25            
26            return $created_offer;
27        }
28    
29    }

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 :

1// app/models/User.php
2    <?php
3    
4    namespace App\Models;
5    
6    use Illuminate\Database\Eloquent\Model;
7    use Illuminate\Database\Eloquent\SoftDeletes;
8    
9    class User extends Model
10    {
11        use SoftDeletes;
12    
13        protected $fillable = [
14            'name', 'email'
15        ];
16        
17        public static function findMultipleEmail($email_list)
18        {
19            // gets id of existing user, if user does not exist, create new user and return users id
20            $users_id = [];
21            foreach ($email_list as $email) {
22                $user_details = static::firstOrCreate(['email' =>$email]);
23                array_push($users_id, $user_details->id);
24            }
25            
26            return $users_id;
27        }
28        
29    }

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 :

1// app/models/Voucher.php
2    <?php
3    namespace App\Models;
4    
5    use Illuminate\Database\Eloquent\Model;
6    use Illuminate\Database\Eloquent\SoftDeletes;
7    
8    class Voucher extends Model
9    {
10        use SoftDeletes;
11    
12        protected $fillable = [
13            'code',
14        ];
15    
16        public function create($offer_id, $users_id)
17        {
18            // Generate 8 random hex code for voucher
19            
20            foreach ($users_id as $key => $user_id) {
21                $vouchers['voucher'][$key]['code']        =   substr(md5(rand()), 0, 8);
22                $vouchers['voucher'][$key]['offer_id']    =   $offer_id;
23                $vouchers['voucher'][$key]['user_id']     =   $user_id;
24            }
25            // insert into the database
26            self::insert($vouchers['voucher']);
27    
28            return $vouchers;
29        }
30     }

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:

1name:Childrens day Special
2    discount:25
3    expires_at:2018-8-25 23:50:49
4    email_list[0]:hello@gmail.com
5    email_list[1]:hey@gmail.com
6    email_list[2]:holla@gmail.com

Your output should look like this:

slim3-postman-create-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

1// routes/web.php
2    [..]
3    $app->post('/offers/create', VoucherController::class . ':createOffers');
4    $app->post('/api/voucher/validate', VoucherController::class . ':validateVoucher');

Open the VoucherController and edit as follows:

1// app/Controllers/VoucherController.php
2    [...]
3     } else {
4                    //return an error on failed validation, with a statusCode of 400
5                    return $response->withStatus(400)->withJson([
6                        'status' => 'Error',
7                        'message' => $validator->getErrors()
8                    ]);
9              }
10      }
11            
12     public function validateVoucher(Request $request, Response $response, $args)
13        {
14            $validator = $this->c->validator->validate($request, [
15                'voucher' => Validator::alnum()->notBlank(),
16                'email' => Validator::email()->noWhitespace()->notBlank(),
17            ]);
18    
19            if ($validator->isValid()) {
20    
21                $voucher    = $request->getParam('voucher');
22                $email      = $request->getParam('email');
23    
24                $voucher_model    =   new Voucher();
25                $user_model       =   new User();
26    
27                // check if user exist
28                $user_details     =   $user_model->findEmail($email);
29    
30                if ($user_details) {
31                    // Assertain that the voucher code belongs to the user and has not expired/not yet used
32                    $validate_voucher =   $voucher_model->validateVoucher($voucher, $user_details->id);
33                    
34                    if (!$validate_voucher->isEmpty()) {
35                        // activate and set date voucher was used
36                        $activate_voucher   =   $voucher_model->activateVoucher($voucher, $user_details->id);
37                        // return voucher details
38                        return $response->withStatus(200)->withJson([
39                            'status'    => (bool) $validate_voucher,
40                            'count'     => count($validate_voucher),
41                            'data'      => $validate_voucher,
42                            'message'   => count($validate_voucher) >= 1 ? 'Success': 'No Voucher found'
43                        ]);
44                    } else {
45                        // return failure message if voucher does not exist
46                         return $response->withStatus(403)->withJson([
47                        'status' => 'Error',
48                        'message' => 'Voucher details is invalid'
49                        ]);
50                    }
51                } else {
52                    // return failure message if user does not exist
53                     return $response->withStatus(400)->withJson([
54                        'status' => 'Error',
55                        'message' => 'User does not exist'
56                        ]);
57                }
58            } else {
59                // return failure message if validation fails
60                return $response->withStatus(400)->withJson([
61                    'status' => 'Validation Error',
62                    'message' => $validator->getErrors()
63                ]);
64            }
65        }
66       [...]

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:

1// app/Models/User.php
2    [...] 
3                return $users_id;
4            }
5        
6        public static function findEmail($email)
7        {
8            return static::where('email', $email)->first();
9        }
10    }

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:

1// app/Models/Voucher.php
2    [...]
3    
4                return $vouchers;
5            }
6        
7    
8     // Assertain that the voucher code belongs to the user and has not expired/not yet used
9        public function validateVoucher($voucher, $user_id)
10        {    
11            $voucher_details = self::leftjoin('users', 'vouchers.user_id', '=', 'users.id')
12                                    ->leftjoin('offers', 'vouchers.offer_id', '=', 'offers.id')
13                                    ->select('vouchers.code', 'users.id as user_id', 'users.email', 'offers.expires_at','offers.name as offer_name','offers.discount as percentage_discount')
14                                    ->where([
15                                                ['vouchers.code', $voucher],
16                                                ['vouchers.user_id', $user_id],
17                                                ['vouchers.is_used', 0],
18                                                ['offers.expires_at', '>', \Carbon\Carbon::now()],
19                                            ])
20                                    ->get();
21                                    
22            return ($voucher_details == null ? [] : $voucher_details);
23        }
24    
25        // activate voucher code, set is_used and date_used fields
26        public function activateVoucher($voucher, $user_id)
27        {  
28            $activate_voucher = self::where([
29                                                ['code', $voucher],
30                                                ['user_id', $user_id],
31                                            ])
32                                    ->update(array('is_used' => 1, 'date_used' => \Carbon\Carbon::now() ));
33    
34            return $activate_voucher;
35     
36        }
37    [...]

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:

1voucher:INSERT-VOUCHER-CODE-HERE
2    email:hello@gmail.com

Your output should look like this:

slim3-postman-validate-voucher

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

1// routes/web.php
2    [..]
3    
4    $app->post('/api/offers/create', VoucherController::class . ':createOffers');
5    $app->post('/api/voucher/validate', VoucherController::class . ':validateVoucher');
6    $app->get('/api/voucher/list', VoucherController::class . ':fetchAllValidVoucherPerUser');

Open the VoucherController and edit as follows:

1// app/Controllers/VoucherController.php
2    
3    [...]
4    } else {
5                return $response->withStatus(400)->withJson([
6                    'status' => 'Validation Error!',
7                    'message' => $validator->getErrors()
8                ]);
9            }
10    }
11    public function fetchAllValidVoucherPerUser(Request $request, Response $response, $args)
12        {
13            $validator = $this->c->validator->validate($request, [
14                'email' => Validator::email()->noWhitespace()->notBlank(),
15            ]);
16    
17            if ($validator->isValid()) {
18    
19                $email = $request->getQueryParam('email');
20    
21                $voucher_model    =   new Voucher();
22                $user_model       =   new User();
23    
24                //check if user exist
25                $user_details     =   $user_model->findEmail($email);
26    
27                if ($user_details) {
28    
29                    //Fetch all valid user voucher codes
30                    $users_voucher =   $voucher_model->fetchSingleUserVoucher($user_details->id);
31    
32                    //return voucher details
33                        return $response->withStatus(200)->withJson([
34                            'status' => (bool) $users_voucher,
35                            'count'     => count($users_voucher),
36                            'data'     => $users_voucher
37                        ]);
38    
39                } else {
40                    //return failure message if user does not exist
41                    return $response->withStatus(400)->withJson([
42                        'status' => 'Error',
43                        'message' => 'User does not exist'
44                        ]);
45                }
46            } else {
47                return $response->withStatus(400)->withJson([
48                    'status' => 'Validation Error',
49                    'message' => $validator->getErrors()
50                ]);
51            }
52        }
53    
54    [...]

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:

1// app/Models/Voucher.php
2    
3    [...]
4            return $activate_voucher;
5     
6        }
7    
8    // method to fetch a single user's voucher details
9        public function fetchSingleUserVoucher($user_id)
10        {    
11            $voucher_details = self::leftjoin('users', 'vouchers.user_id', '=', 'users.id')
12                                    ->leftjoin('offers', 'vouchers.offer_id', '=', 'offers.id')
13                                    ->select('vouchers.code','users.id as user_id', 'users.email', 'offers.expires_at','offers.name as offer_name','offers.discount as percentage_discount')
14                                    
15                                    ->where([
16                                                ['vouchers.user_id', $user_id],
17                                                ['vouchers.is_used', 0],
18                                                ['offers.expires_at', '>',  \Carbon\Carbon::now()],
19                                            ])
20                                    ->get();
21       
22            return ($voucher_details == null ? [] : $voucher_details);
23     
24        }
25    [...]

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:

slim3-postman-list-vouchers

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.