APIfy Your Legacy App with Toro
For the Google Summer of Code 2014, I was selected for a project to create a REST API for ATutor. ATutor has hundreds of thousands of lines of code, yet is written in core PHP. Introducing a PHP router class for the API was necessary, but we needed something unintrusive. In this post, we discuss the essential parts of the project. For this post, all code examples would correspond to my fork of ATutor’s repository (links to files will be provided whenever necessary).
Note – Google Summer of Code is a program where students all around the world can participate in open source projects of mentoring organizations. Google organizes the program and pays the stipends, but the students are not employed by Google at any point during the program.
Web Routing with Toro
The first step in the process was to create or write a PHP class to perform the routing. After considering a few options, we decided to go with Toro, a the light weight PHP router. It performs just the routing – nothing more, nothing less. The syntax is pretty intuitive and you will get started in minutes.
Toro is RESTful- it has support for the standard HTTP methods- GET
, POST
, PUT
and DELETE
. There is support for JSON based requests too. All of this is packed in a 120 odd line file.
Before we proceed, one more step was to configure the server to redirect all requests to the router. This can be performed by adding an .htaccess file in Apache, or changing the configuration file of Nginx. This step of server configuration is explained on the README of Toro’s GitHub repository.
Hello World program
After you have successfully configured your web server, you just need to include the Toro.php
file to perform the routing. Toro matches the incoming URI pattern and sends the request over to a handler for processing. Let us look at the simplest ‘Hello World’ program (from the Toro official site).
class MainHandler {
function get() {
echo "Hello, world";
}
}
Toro::serve(array(
"/" => "MainHandler",
));
There are two blocks of code, which seem pretty intuitive. You provide an associative array to Toro to serve, with the keys matching URL patterns and values containing the handler classes. Toro tries to match the request URI with this array and executes the corresponding handler, depending on the HTTP method. In our example, you will get the desired output of “Hello, World” if you send a GET request to “/” (or the root directory). For POST
, PUT
and DELETE
requests, we define functions post()
, put()
and delete()
, respectively in the handler classes.
Toro gives you the freedom to code the way you like. You could write all the handlers in the same index.php
file and a long list of items in the associative array for Toro to serve. However, the ideal was is to separate the logic into semi-projects and have their own directories, files for handlers and URLs. In index.php
, you could include all those files and use array_merge($urls1, $urls2, ...)
to provide the final array of URLs to Toro.
Separating URLs from rest of the logic
Theoretically, you could kept all your code (handlers, routes, helper classes and functions) in the same page, but that would kill the readability. For the sake of simplicity, it’s imperative that we separate the handlers, routes and helper classes and functions. In fact, a good practice is to create separate directories for the different apps that you are going to create in the project, each having urls.php
and router_classes.php
. Additional files like tests.php
or helper_functions.php
may also be created depending on your project.
In the index.php
file, we include the files containing the routes and use array_merge
to merge all the URLs into one array for Toro. Here’s how it looks.
Toro::serve(array_merge(
$base_urls,
$course_urls,
$student_urls,
$instructor_urls,
// Include the url array of your app
$boilerplate_urls
));
The need of a URL Prefix
We have seen that it is useful to create separate urls.php
files, but how should one such file be structured?
One small fact that you might notice is that the URLs of every app start with the app’s name. A prefix is therefore common to all the URLs of a single app. You should define a prefix for an app and append it to URLs so that you don’t have to repeat it. Here is how a typical urls.php
in my project looks like.
$instructor_url_prefix = "/instructors";
$instructor_base_urls = array(
"/" => "Instructors",
"/:number/" => "Instructors",
"/:number/courses/" => "InstructorCourses",
"/:number/courses/:number" => "InstructorCourses",
"/:number/courses/:number/instructors" => "CourseInstructorList",
"/:number/courses/:number/students" => "CourseEnrolledList"
);
$instructor_urls = generate_urls($instructor_base_urls, $instructor_url_prefix);
You should notice that I have occasionally added a :number
to my URL pattern. A :number
, :string
or :alpha
tells Toro to match the pattern to a number, string or alphanumeric characters to the incoming requests. For instance, /instructors/5/
would match to the second URL and /instructors/5/courses/6/
would match to the fourth.
For the purpose of prepending a prefix to all the URLs, I have created a custom function generate_urls
. You can have a look at it under my core helper functions.
User authentication
Any API that you develop must be able to authenticate users. One way to do so is through the use of tokens. Here are a few things to note about tokens for user authentication.
- Generate token on successful login — In this case, I generate a token by taking the hash of a combination of the user name, timestamp and a random number.
- Set the validity of the token by defining an expiry timestamp after which the token would be unusable.
- The token is present in the header of every request.
- Modify the validity of the token on each successful request and update the expiry timestamp.
- If the token has expired, delete the entry or perform an equivalent task (like set an option in the database that mentions the token as expired).
Developing a backbone function
When you develop an API, you would notice after the implementation of the first few functions that the process for every API call is quite similar. In the simplest of terms, every API call checks if a token is valid, checks if the user has access to that resource, performs an SQL query (or more) and prints a result. A backbone function therefore can therefore be written for executing all such queries. Here are features of such a function.
- Check the validity of the token and determine which user corresponds to that token
- Check if the resource being accessed is available to the user; raise an error in all other cases.
- In case of creating objects, return the ID of the newly created object.
- In case of requests to modify or delete objects, check if the object exists before deleting or modifying it.
- Print the response and log the request.
The function that I developed for my project can be found on GitHub.
Re-using classes for retrieving similar objects
Imagine you are making an API where you need to retrieve users in your system. Look at the following URL types.
"/members/" => "Class1",
"/members/:number/" => "Class2",
The first URL gives you a list of members, whereas the second gives you the details of a certain member. The SQL queries that would be executed in these two cases are going to be very similar — with a simple WHERE
clause in the latter case. We can reuse the same class, MySameClass
for both the URLs, which can be defined as follows —
class MyClass {
function get($member_id) {
...
if ($member_id) {
$query = $connection->prepare("SELECT col1, col2, col3 FROM table_name WHERE id = :member_id");
$query->bindParam(':member_id', $member_id, PDO::PARAM_INT);
}
// Execute the query or do something else
$query->execute();
$result = $query->fetchAll();
...
}
}
The simple logic here is that in case of the first URL, $member_id
in my function would be empty, whereas in the latter case, the WHERE
clause is appended.
Final thoughts
In this post, I have discussed the basic idea of creating an API in PHP using Toro as the router. I focused on a few areas that might challenge the efficiency and readability of the code that you write. What I have have talked about may not be the only way of doing certain things, and you might have even better ideas of performing the same action.
If you have better ideas about what we have discussed here — from shorter to more efficient ways of doing certain tasks, feel free to comment below.