0
\$\begingroup\$

I have just written an API framework which I am hoping to implement into one of my large sites very soon. However, before I do so, I want to be sure that the script is well written and there are no obvious mistakes that I have missed.

As with most projects, the key here is automation and speed... I have tried to automate as much as possible whilst also keeping expensive code to a minimum.

These are a few sample requests that one could fire at this API:

// Return the logged in users details
http://api.example.net/user/

// Return the registered companies for the logged in user
http://api.example.net/user/companies/

Where user is the $object on both and 'default' is the $request on the first and 'companies' is the $request on the second...

The data is then retrieved based on the $request on the $object... Make sense? :-) I hope so, here is my code:

<?php

    // +------------------------------------------------------------------------+
    // | api.php                                                                |
    // +------------------------------------------------------------------------+
    // | Copyright (c) Example Ltd 2012. All rights reserved.                   |
    // | Version       1.0                                                      |
    // | Last modified 14/05/2013                                               |
    // | Email         dev@example.net                                          |
    // | Web           https://api.example.net                                  |
    // +------------------------------------------------------------------------+

    /*
     * Example API
     *
     * @version   1.0
     * @author    Ben Carey <ben.carey@example.net>
     * @copyright Example Ltd
     *
    */

    // Guidelines for creating and adding a working method
    // 1. All functions must have $parameters=array() as their only parameter
    // 2. All must return some sort of data
    // 3. Set the $example->json_constants if you need to specify anything about the JSON output
    // 4. Any errors place into $example->error (string) and return
    // Once the method has been created, you can proceed to adding it to the API
    // 1. Work out what address you want e.g. api.example.net/user/companies
    //      -> This example will translate to the domain api.example.net/api.php?object=user&request=companies
    // 2. If the object has a corresponding class and the class does not already exist in the defined list then add it
    // 3. If the object does not have a corresponding class, it is an alias so add it (if not already there), to the alias array
    // and map it to the correct class
    // 4. Now you need to map the request to the method. If there is no request set (e.g. /user instead of /user/companies) then the request
    // is treated as 'default'. You can then specify in the $api_methods array $object -> $request -> $method

    // Define the allowed domains,
    // this will prevent other sites from being able to
    // access the API via AJAX
    $allowed_domains = array('http://beta.example.net','https://beta.example.net');

    // Test if the domain is allowed to connect
    if(isset($_SERVER['HTTP_ORIGIN'])&&in_array($_SERVER['HTTP_ORIGIN'],$allowed_domains)){
        // Allow access to the current domain
        header('Access-Control-Allow-Origin: '.$_SERVER['HTTP_ORIGIN']);
    }

    // Include the site configuration
    include('C:\Inetpub\vhosts\example.net\subdomains\beta\httpdocs\_snippets\_system\config.php');

    // Define the Example Classes
    // All of these paths link to the class initiation file,
    // in other words, by including them it will include all
    // necessary files and create an instance of the class
    $classes = array(
        'authorized'    => $_PATHS['EXAMPLE'].'\_snippets\_classes\class.authorized.php',
        'documents'     => $_PATHS['EXAMPLE'].'\_snippets\_classes\class.documents.php',
        'reports'       => $_PATHS['EXAMPLE'].'\_snippets\_classes\class.reports.php',
        'settings'      => $_PATHS['EXAMPLE'].'\_snippets\_classes\class.settings.php',
    );

    // Create a map of aliases to classes
    // This will allow objects that do not have a matching class but
    // but do have a corresponding method in another class
    // specified here
    $class_aliases = array(
        'user'      => 'authorized',
        'company'   => 'authorized'
    );

    // Gather the relevant methods for the supplied
    // request method
    switch($_SERVER['REQUEST_METHOD']){
        case 'GET':
            // Create an array that maps the objects to the 
            // relevant request and method within the class
            $api_methods = array(
                'authorized'    => array(
                    'default'               => 'get_authorized'
                ),
                'documents'     => array(
                    'sessions'              => 'get_active_sessions'
                ),
                'masterfiles'   => array(
                    'items'                 => 'get_master_file_items',
                    'properties'            => 'get_master_file_properties',
                    'reference'             => 'get_master_file_reference'
                ),
                'user'          => array(
                    'companies'             => 'get_registered_companies_for_user'
                )
            );
            // Set the parameters to be passed to the corresponding
            // method, this is set depending on the page request method
            $parameters = $_GET;
        break;
        case 'POST':
            // Create an array that maps the objects to the 
            // relevant request and method within the class
            $api_methods = array(
                'settings'  => array(
                    'user' => 'update_user'
                )
            );
            // Set the parameters to be passed to the corresponding
            // method, this is set depending on the page request method
            $parameters = $_POST;
        break;
    }

    // The object and request are always supplied via GET
    $object = strtolower($_GET['object']);
    $request = strtolower($_GET['request']);

    // Set the class to include based on the object supplied
    if(isset($classes[$object])){
        $class = $classes[$object]; 
    }elseif(isset($class_aliases[$object])){
        $class = $classes[$class_aliases[$object]];
    }else{
        $data = array('error' => 'Specified object does not exist');    
    }   

    // Set the method to execute based on the request supplied
    if(!empty($request)&&isset($api_methods[$object][$request])){
        $method = $api_methods[$object][$request];
    }elseif(isset($api_methods[$object]['default'])){
        $method = $api_methods[$object]['default'];
    }else{
        $data = array('error' => 'Specified request does not exist');
    }

    // Only proceed if there are no errors
    if(empty($data)){

        // If the request is via AJAX then prevent all redirects
        if(isset($_SERVER['HTTP_X_REQUESTED_WITH'])&&strtolower($_SERVER['HTTP_X_REQUESTED_WITH'])=='xmlhttprequest'){
            $prevent_redirect = true;
        }

        // Include the relevant class
        include($class);

        // Remove the request and method values from the parameters
        unset($parameters['object'],$parameters['request']);

        // Execute the relevant method
        $data = call_user_func(array($example,$method));

        // If there was an error then replace the data variable
        $data = !empty($example->error) ? array('error' => $example->error) : $data;
    }

    echo json_encode($data,$example->json_constants);

?>
\$\endgroup\$
2
\$\begingroup\$

Ignoring your code for a second, here's some feedback on the design of your API.

It may be better to simply include the user's companies in the response for the /user endpoint. This would make your code more complex, but API consumers' lives easier.

Obviously removing the /user/companies endpoint eliminates the ability to request ONLY company data for a specific user. If you feel it's important to be able to request this information on its own, then here's two options.

  1. Allow partial responses via GET parameters (i.e. GET /user?fields=companies) or something similar with JSON in the request body.
  2. Decouple the companies endpoint from the /user endpoint and create a top-level /companies endpoint. Provide a way to query by user ID, and you could get the same data as /user/company

As a final note, if you ever plan on allowing queries for information on users other than the current user, you may want to use a "me" placeholder in the endpoint like so: /user/me. If you were to use a modern routing library like Slim, you can easily support queries for the current user and arbitrary users simply by parsing everything after the slash (/) like so:

$app->get('/user/:user_id', function ($user_id) {
    if ('me' == $user_id) {
        // Get the current user
    } else {
        // Get user with ID $user_id
    }
    ...
});
\$\endgroup\$
1
  • \$\begingroup\$ Thank you for your answer. The user and companies are just there for illustrative purposes. This API is for a very large web application so splitting up the requests is absolutely essential as it minimises the number of queries executed by one script, thus improves efficiency of the whole site. As for the user and 'me' placeholder, that is a handy idea that I will definitely implement into some of my other applications but for this it is not really relevant. +1 for your contribution :-) \$\endgroup\$
    – Ben Carey
    May 14 '13 at 17:42
1
\$\begingroup\$

tl;dr . No seriously, your code is really hard to read. I'm not sure if you added some comments only for this question or if it is the a exact copy of your file, but it is to much. Every time you have the feeling that you need a comment do explain your code, think about extracting a variable or a method with a suitable name. Write a method setAccessControlHeader(), loadConfig(), loadMapping(),getClassFromRequest(), getMethodFromRequest(), etc . All this will help you and any other reader of your code to get a first overview of your code without digging into the details how e.g. you mapping is implemented. Anybody who is interested in the details can easily look in the method anyway. The main benefit, beside a more obvious structure, is the easier testability, as you can test every method at it's own.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.