APIfy Your Legacy App with Toro

Shaumik Daityari
Tweet

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.

Free JavaScript: Novice to Ninja Sample

Get a free 32-page chapter of JavaScript: Novice to Ninja and receive updates on exclusive offers from SitePoint.

  • Bruno Škvorc

    Thanks!

  • http://dada.theblogbowl.in/ Shaumik Daityari

    Thanks for noticing. It must be temporary because the GitHub page still links to toroweb.org.

  • http://blog.gslin.org/ Gea-Suan Lin

    Using addslashes() for escaping is not a good idea.

    • ccornutt

      Agreed – please update this to use prepared statements instead. Even if it’s “just an example” it’s an example that people will likely cut and paste and assume is correct.

  • CTN

    Thanks for this article. I’ve been meaning to get into adding modern routing to my php apps. What I don’t like about toro is that you have to create a unique class per route. I found a number of alternatives such as AltoRouter and the Slim framework, but I think PHP-Router is one I’m going to use:
    github .com/dannyvankooten/PHP-Router

  • Ivan Panfilov

    create real folder with files

    /instructors/index.php
    /courses/index.php
    /students/index.php

    then use requests

    /instructors/?id=1
    /instructors/?id=1&courses=5

    then u dont neead any useless routers

    • http://dada.theblogbowl.in/ Shaumik Daityari

      Hi Ivan,

      I agree with using such ids for retrieving objects, but what if you have URLs like the following-

      /courses/id/tests/

      Which gives me a list of tests. True I could do it with an extra GET variable but don’t you think it would be more readable (and logical) this way.

      • Ivan Panfilov

        i don’t think using parameters in query it’s made less readability.