API Project - Part 5 - Controllers
In this step we'll create controller classes.
Remember that when we define a route, we specify which controller class should be instantiated, and which method of the instance should be invoked (the action method).
There are many differing opinions about exactly what a controller should (and should not) do. The simplest definition is that a controller should handle the 'business logic' of an application.
Our controllers will do the following:
- Handle specific HTTP requests and create HTTP responses, which includes:
- Validating user input
- Managing the transfer of data between the server and the client
- Controlling security and authorization
All of the controllers that we create will extend the Controller class (which you'll find in the controllers folder). The Controller class has some methods that may be useful for handling HTTP requests, and are inherited by all sub classes. We'll most likely be talking about some of these methods in class.
Lets's create a controller for working with user roles.
This is the fun part! We get to start using the components that we have worked so hard to create (and have not gotten to do anything useful with, other than run tests). Once we get this controller up and running, you'll finally get to see how all of the pieces work together in order to handle requests and create responses.
As we get further into creating controllers, it becomes more and more important for you to be aware of the status headers that should be used when handling requests.
Add a file to the controllers folder named RoleController.inc.php and put this code in it:
<?php
include_once("Controller.inc.php");
include_once(__DIR__ . "/../models/Role.inc.php");
include_once(__DIR__ . "/../dataaccess/RoleDataAccess.inc.php");
class RoleController extends Controller{
function __construct($link){
parent::__construct($link);
}
public function handleRoles(){
$da = new RoleDataAccess($this->link);
switch($_SERVER['REQUEST_METHOD']){
case "POST":
echo("TODO: INSERT ROLE");
break;
case "GET":
echo("TODO: GET ALL ROLES");
break;
default:
// set a 400 header (invalid request)
$this->sendStatusHeader(400);
}
}
}
We'll talk about this code in a moment, but first navigate to this page in your browser: http://localhost/api/roles.
If you see an error message about accessing the database: check the config file and make sure that the DB_PASSWORD constant is defined properly (for Windows it should be an empty string, but for Ubuntu it should be 'test').
You should see a message that says 'TODO: GET ALL ROLES'.
Now let's talk about the code:
- It imports/includes the Controller base class, the Role, and the RoleDataAccess class (I had to use PHP's built in DIR constant in order to properly set the paths to each file, let me know if you have questions about it)
- The RoleController class has a constructor function and a method named handleRoles().
- The handleRoles() method instantiates its data access class (RoleDataAccess), and then has a SWITCH statement that looks at the method of the incoming request. If the request is a POST, then we echo TODO: INSERT ROLE. If the request is a GET, then we echo TODO: GET ALL ROLES. Finally, look at what the default case does...it calls the sendStatusHeader() method, which is inherited from the Controller class. The parameter (400) will result in the app responding with a 400 error, which indicate that the request is not valid. The handleRoles() action method will handle POST and GET requests. Any other request method, such as PUT and DELETE, will result in a 400 status header in the response.
Now, let's begin to add code to get all roles. Replace the line that says echo("GET ALL ROLES"); with this (make sure to indent the code properly after you paste it into the proper place):
// get all Roles
$roles = $da->getAll();
//print_r($roles);die(); // sanity check (ask me about 'sanity checks')
// convert the Roles to JSON
$jsonRoles = json_encode($roles);
// set the headers (headers must be set before echoing anything)
$this->setContentType("json");
$this->sendStatusHeader(200);
// set the response body
echo($jsonRoles);
// terminate
die();
We need to go through one of these lines of code, but first reload the page in your browser (http://localhost/api/roles) and bask, for a moment, in the glory that you have created!!!
You have created a web service (specificially a REST API) will respond to GET requests for http://localhost/api/roles and do so by adding a JSON string to the body of the response. The client who made the request may do whatever it wishes with the string. But most likely, it will parse it into JavaScript and then take it from there (quick side note: the Axios library that we are using on the JavaScript side automatcially parses the string for us).
The is a major step! And before moving on, you should step through the code with the debugger by setting the break point somewhere in index.php. I suggest putting on the line that instantiates the Router, so that you can step through the code from the very beginning of the process.
Now let's talk about the code that we just added.
- It gets all the roles from the database. And since we wrote the code for getAll(), we now that there is quite a bit going on under the hood to make it happen. The result will be an array of Role objects.
- The array of roles is converted into a JSON string so that it can be added to the HTTP response (PHP's json_encode() function is the equivalent JSON.stringify() in JavaScript).
- The inherited setContentType() method will just a header that tells the client the data has been encoded into JSON.
- The sendStatusHeader(), which is also inherited, set the status of the response to 200 (hopefully you know what that means by now, if not please ask me).
- The JSON string is echoed into the body of the response.
- The script terminates, and ends the response.
Step through the code with the debugger a few more times and get familiar with the entire process. Step into some of the methods that we have already created, so that you can see your code in action and get more familiar with it. Step into anything you want! You should be able to understand all of it, with the exception of the Router class (that's some nasty code, and you don't need to worry about it as long as you understand what the Router does).
UserController
Now we'll create a controller for managing users. If you look in index.php, at the $routes variable, you'll see that there are two routes that will make use of the UserController. These two routes have different action methods. So in this step, we will create a UserController class that defines two action methods (handleUsers() and handleSingleUser()).
We might want to talk about regular expressions before we move on! You'll see some regular expressions in the code we write next. In this course we don't read about them until later, so I'm not sure how familiar you'll be with them at this time. Don't worry, in order to understand the following code, it's not critical that you understand regular expressions.
Add a file named UserController.inc.php to the controllers folder.
Paste the following code into the file:
<?php
include_once("Controller.inc.php");
include_once(__DIR__ . "/../models/User.inc.php");
include_once(__DIR__ . "/../dataaccess/UserDataAccess.inc.php");
class UserController extends Controller{
function __construct($link){
parent::__construct($link);
}
// We'll add methods here
}
Hopefully you understand this code by now. If there is anything that you aren't sure about, please ask me to explain.
It's important to note that you must have your User model and your UserDataAccess class completed in order to complete this class.
The handleUsers() Action Method
Now we'll add the first action method. Put this code inside of the class:
// action method for the /users route
public function handleUsers(){
$da = new UserDataAccess($this->link);
switch($_SERVER['REQUEST_METHOD']){
case "POST":
echo("INSERT USER");
break;
case "GET":
echo("GET ALL USERS");
break;
case "OPTIONS":
// AJAX CALLS WILL OFTEN SEND AN OPTIONS REQUEST BEFORE A PUT OR DELETE
// TO SEE IF CERTAIN REQUEST METHODS WILL BE ALLOWED
header("Access-Control-Allow-Methods: GET,POST");
break;
default:
// set a 400 header (invalid request)
$this->sendStatusHeader(400);
}
}
The code in this method should look familiar to you because it's so similar to what we did in the RoleController class. But this method instantiates a UserDataAccess object (rather than a RoleDataAccess object) and the SWITCH statement includes a case for an OPTIONS request. By now, you have certainly learned about POST and GET requests, but I'm not sure if you'll have encountered OPTIONS requests. I don't want to get into the details about them right now, but they are used and AJAX calls are made. We can talk about them in class, please ask me about this. We will certainly have to deal with OPTIONS requests when we incorporate a front end into this project.
Navigate to this URL in your browser: http://localhost/api/users/
You should see a message saying TODO: GET ALL USERS.
Getting All Users
Now replace the line of code that echos 'TODO: GET ALL USERS' with this:
$users = $da->getAll();
//print_r($users);die();
// Convert the users to json (and set the Content-Type header)
$json = json_encode($users);
// set the headers (before echoing anything into the response body)
$this->setContentType("json");
$this->sendStatusHeader(200);
// set the response body
echo($json);
die();
This code is almost indentical to what we did in the RoleController. The only difference is that using a different data access class.
If you navigate again to http://localhost/api/users/ you should see the JSON string (users from our database) that has been echoed into the body of the response.
Posting a New User
Now let's turn our attention to the POST requests, replace the line that says echo("TODO: INSERT USER"); with this code:
// parse the JSON sent (in the request body) into an associative array
$data = $this->getJSONRequestBody();
//print_r($data);die(); // sanity check!
// pass the associative array into the User contructor
$user = new User($data);
//print_r($user);die(); // another sanity check!
// make sure the User is valid, if so TRY to insert
if($user->isValid()){
try{
$user = $da->insert($user);
$json = json_encode($user);
$this->setContentType("json");
$this->sendStatusHeader(200);
echo($json);
die();
}catch(Exception $e){
$this->sendStatusHeader(500, $e->getMessage());
die();
}
}else{
$msg = implode(',', array_values($user->getValidationErrors()));
$this->sendStatusHeader(400, $msg);
die();
}
There's some tricky code here, let's try to walk through it:
We get the data that has been sent in the body of the request by using the inherited getJSONRequestBody() method. This method will parse the JSON string sent in the body of the request, and return it as an associative array. Feel free to inspect the code for this method in the Controller class (ask me questions, it uses some strange PHP code in my opinion).
So, for example, if the body of the HTTP request looks like this (remember, this is a string):
{"firstName":"Fred", "lastName": "Jones", "email": "fj@fj.com", "password":"test", "roleId":1, "active": true}
Then getJSONRequestBody() will return an associative array that has has keys for firstName, lastName, email, etc.
We then pass this array into the constructor for the User class.
Next we check to see if the User object is loaded with valid data (by calling isValid()). If the User is valid, we TRY to do the following:
- Insert it into the database by calling the insert() method of the data access class. Remember that the insert() method will return a User object that includes it's brand new ID.
- Convert the User object into a JSON string
- Set some headers (we've seen this before)
- Echo the JSON string into the body of the response (we've seen this before)
- Terminate with die();
- If we end up inside the CATCH block, it most likely indicates a database error occurred while trying to run the insert. In this case, the handleError() method of the DataAccess class will have been triggered. It is designed to throw an excepting that we are now catching. The error message should indicate why the insert failed. We set the status header to 500 (which means 'internal server error') and we add the error message to the header.
If the User is not valid, we do the following:
- get all the validation errors and parse them into a single string (stored in the $msg variable)
- set the HTTP status header to 400 (which means the request is not valid) and include the message in the header
- terminate with die():
There's a lot going in this code! If you can understand all of it then you are doing great! Unfortunately we can't just paste a URL into the browser to run this code because it's triggered by a POST request. We could use a tool such as PostMan to make post requests, but for now I've created a crude HTML page that allows you to run tests against the UserController class.
Navigate to http://localhost/api/tests/UserControllerTests.html in your browser.
When you press the POST User button, input entered into the textbox will be sent in the body of the request (note that it's JSON string). .
Make sure to use PHPMyAdmin to verify that the rows have been added to the users table.
TRY THIS: See what happens if you try to insert a user with an email address that already exists in the database. Use the console log and the network tab and see if you can find the database error message (we are passing it through with the status header of the HTTP response)
The handleSingleUser() Action Method
Now let's turn our attention to routes that look like this: /users/:id. These routes should include a valid user ID (in place of the :id). Our router is configured to instantiate the UserController class for this route, and then to call the handleSingleUser() action method. So we need to add this method to the UserController class:
public function handleSingleUser(){
// We need to get the url being requested so that we can extract the user id
$url_path = $this->getUrlPath();
$url_path = $this->removeLastSlash($url_path);
//echo($url_path); die();
// extract the user id by using a regular expression
$id = null;
if(preg_match('/^users\/([0-9]*\/?)$/', $url_path, $matches)){
$id = $matches[1];
}
$da = new UserDataAccess($this->link);
switch($_SERVER['REQUEST_METHOD']){
case "GET":
echo("TODO: GET USER $id");
break;
case "PUT":
echo("TODO: UPDATE USER $id");
break;
case "DELETE":
echo("TODO: DELETE USER $id");
break;
case "OPTIONS":
// AJAX CALLS WILL OFTEN SEND AN OPTIONS REQUEST BEFORE A PUT OR DELETE
// TO SEE IF THE PUT/DELETE WILL BE ALLOWED
header("Access-Control-Allow-Methods: GET,PUT,DELETE");
break;
default:
// set a 400 header (invalid request)
$this->sendStatusHeader(400);
}
}
The first two lines of code inside the body of the method allow us to get the path of the URL in the request. It could look something like /users/7 or /users/7/.
Then we use a regular expression to extract the ID out of the URL.
Next we instantiate the data access class.
Finally, we have a SWITCH statement that is controlled by the METHOD of the request.
Before moving on, you may want to try out the other buttons in our test page (localhost/api/tests/UserControllerTests.html). Since we haven't completed the code for the SWITCH statement, you'll see various TODO messages in the body of each response.
Getting a User By ID
Replace this line: echo("TODO: GET USER $id"); with the following code:
$user = $da->getById($id);
$json = json_encode($user);
$this->setContentType("json");
$this->sendStatusHeader(200);
echo($json);
die();
Hopefully all of this code will look familiar to you. If you aren't sure about anything, please ask me about it and we can discuss it.
Now test it by pressing the Get User By ID button in the test page and look at the response in the console log.
Putting (Updating) a User
Replace this line of code: echo("TODO: UPDATE USER $id"); with the following code:
$data = $this->getJSONRequestBody();
$user = new User($data);
if($user->isValid()){
try{
if($da->update($user)){
$json = json_encode($user);
$this->setContentType("json");
$this->sendStatusHeader(200);
echo($json);
}
}catch(Exception $e){
$this->sendStatusHeader(500, $e->getMessage());
}
die();
}else{
$msg = implode(',', array_values($user->getValidationErrors()));
$this->sendStatusHeader(400, $msg);
die();
}
This is almost identical to what we did for POST requests. The only difference is that we are now calling the update() method of the data access class.
In the test page, press the PUT User button to make a PUT request. See what happens if you enter a user ID that does not exist (you should get a 500 status with error message). Also, see what happens if you try to update a user with an email address that is alread in use by another user.
Make sure to use PHPMyAdmin to verify that the database is getting updated.
Deleting a User
We won't actually delete any rows from the users table. This can cause problems with foreign keys, and some people believe that you should never really delete any data. Instead we'll do what's known as 'virtual delete' by make the user inactive.
Replace this line: echo("TODO: DELETE USER $id"); with the following code:
// instead of deleting the row, we'll make the user inactive
if($user = $da->getById($id)){
$user->active = false;
$da->update($user);
$this->sendStatusHeader(200);
}else{
$this->sendStatusHeader(400, "Unable to 'delete' user $id");
}
Hopefully this code makes sense. If not, please ask for help.
Go ahead and test out a delete by pressing the Delete User button on the test page.
We have now created a controller that will handle CRUD operations for the users table!!!
Here is a quick diagram that documents our REST API:
URL(route) METHOD Description Controller Action Method
------------------------------------------------------------------------------------------------------
users/ GET gets all users UserController handleUsers
users/ POST inserts a user UserController handleUsers
users/1 GET gets user with id of 1 UserController handleSingleUser
users/1 PUT updates user with id of 1 UserController handleSingleUser
users/1 DELETE deletes user with id of 1 UserController handleSingleUser
roles/ GET gets all roles RoleController handleRoles
login/ POST authenticates a user LoginController handleLogin
logout/ GET kills a users session LoginController handleLogout
TODO: Add your API calls here
You have already built the RoleController and the UserController, and we will be building the LoginController in the next step.
For your final project, you'll have to add information based on your own database design.