Concepts

To create an API endpoint that returns either JSON or CSV you implement a class that extends the abstract API endpoint base class. Within a required method called doExecute you must return an array. Depending on the requested result format the array your method returns is serialized into either JSON or CSV (currently no other formats are supported).

What is the base class?

The base class that must be extended is called AbstractRestExposedFunction and lives in lib/REST. See here.

Where to I put my endpoint class?

All classes that should be exposed as API endpoints are located in apps/frontend/lib/REST. Related endpoints should be grouped in subfolders. The subfolders themselves do not have any influence over the route of the endpoint, they are simply an organizational structure for the code.

What should I name my class?

Symfony 1.x does not use PHP namespaces, therefore your class name MUST be globally unique! A safe way to do it is to combine the namespaces and the actual endpoint name followed by the method name.

Example:

A GET method that has the following route /callcenter/contact.

CallcenterContactGET

How do I return JSON?

You don't! The doExecute() method is the main entry point into the API endpoint class and must always return an array of records or throw an exception. The serializer, which is part of the framework, will detect what the request format is and will serialize the array into the proper result format. Currently only JSON and CSV are supported.

How do I return an error?

If your doExecute() method throws an exception of type Exception the user/client will get a 500 Internal Server Error response in the format that was requested.

If you need to return a different error you can do so by throwing special Exceptions. See Exception handling.

Development Environment

Installing Symfony 1.4.17

Make sure an svn client exists on your machine. Most UNIX machines come with svn already installed. In the command line, run

$ svn http://svn.symfony-project.com/tags/RELEASE_1_4_17 /usr/local/share/symfony1.4.17

Configuring database.yml

Using /config/databases.yml.dist as a reference, configure correct hosts and user credentials for your own database connections. Save this file as /config/databases.yml. In most cases, you would have to create databases/schemas on your localhost MySQL server. You can download the community edition of MySQL at the official website.

Also, be sure to have the following databases/schemas in your localhost (As of Oct 2015). They can be empty. * intranet * laserspine * logging * lsi_api * lsi_call_center * lsi_dataautomation * lsi_referral_center * lsi_report_center * report

Command line tricks for lazy people

Alias all the things!

When working in Symfony 1 or 2 you have to type fairly long commands to just call the console app. Adding the following to your .profile or .bashrc (depending on how you have your env setup) will allow you to call symfony with a single character (or two):

# Syfmony 1 aliases
alias s='./symfony'

# Symfony 2 aliases
alias sf='app/console'

Now instead of having to type

$ ./symfony namespace:commandname

you can simply type

$ s namespace:commandname

Likewise, if you have projects you are working on frequently and need to change into different directories for them you should definitely alias these as well:

alias api='cd ~/Documents/www/lsi/lsiapi'

The above will allow you to jump to the lsiapi project by simply typing api at the prompt. Simply repeat for any other project.

Once you have edited your dot file (.profile) you need to source it again for the changes to take effect in the current terminal session.

$ . .profile

Use git prompt

When using git on the command line it is very helpful to have the branch name and possibly other information showing in the prompt. Take a look at this to get you started on setting that up.

Setting up the project code base for the first time

When installing the project for the first time by cloning it from github it is necessary to also clone the git submodules. To do this first clone the project repository:

$ git clone git@github.com:laserspine/lsiapi.git

Then clone the git submodules:

$ cd lsiapi
$ git submodule update --init

Coding Standards

Following coding standards is one of the easiest ways for everybody to understand everybody else's code.

Here's the golden rule(s):

  • Imitate the existing symfony 1.x code for legacy code or reformat it to sf2
  • Adhere to the symonfy 2 coding standards for ALL new code

Are you implementing a new endpoint?

  • Yes? Then go here SF2 docs
  • No? Working on legacy code? Read on.

Symfony 1.x standards (for legacy code ONLY)

Never use tabulations in the code. Indentation is done by steps of 2 spaces:

<?php
class sfFoo
{
  public function bar()
  {
    sfCoffee::make();
  }
}

Don't put spaces after an opening parenthesis and before a closing one.

<?php
if ($myVar == getRequestValue($name))    // correct
if ( $myVar == getRequestValue($name) )  // incorrect

Use camelCase, not underscores, for variable, function and method names:

  • Good: function makeCoffee()
  • Bad: function MakeCoffee()
  • Bad: function make_coffee()
  • An exception regarding the latter: use underscores for helper functions name (only for symfony 1.0 stuff).

Use underscores for option/argument/parameter names.

Braces always go on their own line.

Use braces for indicating control structure body regardless of number of statements it contains.

Symfony is written in php5, so every class method or member definition should explicitly declare its visibility using the private, protected or public keywords.

Don't end library files with the usual ?> closing tag. This is because it is not really needed, and because it can create problems in the output if you ever have white space after this tag.

In a function body, return statements should have a blank line prior to it to increase readability.

<?php
function makeCoffee()
{
  if (false !== isSleeping() && false !== hasEnoughCaffeineForToday())
  {
    canMakeCoffee();

    return 1;
  }
  else
  {
    cantMakeCoffee();
  }

  return null;
}

All one line comments should be on their own lines and in this format:

<?php
// space first, with no full stop needed

Avoid evaluating variables within strings, instead opt for concatenation:

<?php
$string = 'something';
$newString = "$string is awesome!";  // bad, not awesome
$newString = $string.' is awesome!'; // better
$newString = sprintf('%s is awesome', $string); // for exception messages and strings with a lot of substitutions

Use lowercase PHP native typed constants: false, true and null. The same goes for array(). At the opposite, always use uppercase strings for user defined constants, like define('MY_CONSTANT', 'foo/bar'). Better, try to always use class constants:

<?php
class sfCoffee
{
  const HAZ_SUGAR = true;
}
var_dump(sfCoffee::HAZ_SUGAR);

To check if a variable is null or not, don't use the is_null() native PHP function:

<?php
if (null !== $coffee)
{
  echo 'I can haz coffee';
}

When comparing a variable to a string, put the string first and use type testing when applicable:

<?php
if (1 === $variable)

Use PHP type hinting in functions and method signatures:

<?php
public function notify(sfEvent $event)
{
  // ...
}

All function and class methods should have their phpdoc own block:

  • All @... statements do not end with a dot.
  • @param lines state the type and the variable name. If the variable can have multiple types, then the mixed type must be used.
  • Ideally @... lines are vertically lined up (using spaces):
<?php
/**
 * Notifies all listeners of a given event.
 *
 * @param  sfEvent  $event  A sfEvent instance
 *
 * @return sfEvent          The sfEvent instance
 */
public function notify(sfEvent $event)

Method Types

Try to follow the general rules for HTTP verbs:

GET ...... Fetch one or more resources
PUT ...... Update an existing resource (or create a new one with a given id)
POST ..... Create a new resource (when the id is not known)
DELETE ... Delete an existing resource

Other verbs are not yet supported, but can certainly be implemented.

Query Parameters

Make use of the available "framework" to handle query string parameters.

Defining query parameters

In the configure method you can use addParameters() to add known parameters to the endpoint. These will automatically be processed and made available in the endpoint class. Validation is performed depending on the defined type passed to the RestParameter constructor.

protected function configure()
{
  $this->addParameters(array(
      new RestParameter(
        'id',
        RestParameter::PARAMETER_OPTIONAL,
        RestParameter::TYPE_NUMERIC,
        'The id of the record'
      )
  ));

RestParameter

The class to create a parameter has the following constructor signature

RestParameter($name, $mode, $type, $help)

$name

The name of the parameter following the naming conventions below.

$mode

Specify whether the parameter is required or not. Use the available constants RestParameter::PARAMETER_REQUIRED and RestParameter::PARAMETER_OPTIONAL. If the parameter is specified as required and is not present in the query string then the endpoint will throw a 400 - Bad Request error.

$type

There are a number of basic types that can be specified that will be validated. If a type is not explicitly available use TYPE_TEXT and perform your own validation.

  • TYPE_TEXT - Any type of character string

  • TYPE_NUMERIC - A numeric value (int or float)

  • TYPE_DATE - A YYYY-MM-DD formatted date string

  • TYPE_DATEYEAR - A YYYY formatted date string

  • TYPE_DATETIME - A YYYY-MM-DD HH:MM:SS formatted datetime string

  • TYPE_BOOLEAN - A boolean value. Returns TRUE for 1, true, on and yes. Returns FALSE for 0, false, off, no and '' (empty). Throws 400 exception on all other values.

  • TYPE_INTVECTOR - Expects a ; separated list of integer values and parses them into an array of integers

$help

A plain English description of the parameter. This is used to auto-generate API documentation.

Accessing the query parameters defined in configure()

$input = new Input($this);

Get access to the query string parameters by instantiating an instance of Input as shown above.

IMPORTANT This is fairly new and you will see many legacy endpoints that do not use the Input object at all. They instead use getParameter* methods directly available on the endpoint class. Try to avoid that from now on!!

A more recent example of how this is used can be found here

To access the values for specific parameters use the following syntax:

$resultSchema = $input->getParameter('resultschema', false);

If the parameter resultschema was declared as boolean and is optional, then the $resultSchema variable will be assigned true or false depending on the value in the query string. If the parameter is not present in the query string the variable will be assigned boolean false because it was declared as optional and the second parameter to getParameter() is false (default value if not present).

Query parameter naming conventions

  • Use all lower case names
  • Separate words with "_" (updated_at, not updatedAt)

Built-in parameter names (reserved words)

  • offset
  • rowcount
  • jsonp_callback
  • resultschema
  • mode
  • api_key

Commonly used parameters

For parameters that are common/frequently used try to stick to the following in order to create a more uniform experience across the API.

from ... A data or datetime that represents a starting time for a period (inclusive)
to ..... A date or datetime that represents a ending time for a period (inclusive)

Routes

The route of an endpoint is specified in the endpoint class as a string value for the route member variable and is used by Symfony's router to match the incoming request to the doExecute() method of said class.

Default usage

The syntax for the value of route is as follows:

$this->route = '/foo/bar';

It must start with a leading / and should end with the name of the resource that it operates on.

For the above example:

  • /foo is part of the path or namespace that can be used to group similar endpoints.
  • bar is the name of the resorce

The API build command symfony api:rebuild will take this route value (and other parameters specified in the class) and build the following entry in the apps/frontend/config/routing.yml file:

rest_foobarget:
  url: '/foo/bar.:sf_format/*'
  class: sfRequestRoute
  param: { module: api, action: foobarget, sf_format: json }
  requirements: { sf_method: [get], sf_format: '(?:json)' }

This route will match

  • /foo/bar
  • /foo/bar?param=1
  • /foo/bar.json
  • /foo/bar.json/param/1
  • /foo/bar.json?param=1

It will NOT match

  • /foo/bar/param/1

The reason this route will not match is that the parser cannot determine where the path ends the query string parameters begin.

"Expert" mode

If you know what you are doing you can pass the entire route construct instead of letting the generator build it for you.

<?php
$this->route = array(
    'definition' => array(
        'rest_operationcenteroperationcenterget1' => array(
            'url'   => '/operationcenter/:id',
            'class' => 'sfRequestRoute',
            'param' => array(
                'module'    => 'api',
                'action'    => 'operationcenteroperationcenterget',
                'sf_format' => 'json'
            ),
            'requirements' => array(
                'sf_method' => array('get'),
                'sf_format' => '(?:json)'
            )
        ),
        'rest_operationcenteroperationcenterget2' => array(
            'url'   => '/operationcenter.:sf_format/*',
            'class' => 'sfRequestRoute',
            'param' => array(
                'module'    => 'api',
                'action'    => 'operationcenteroperationcenterget',
                'sf_format' => 'json'
            ),
            'requirements' => array(
                'sf_method' => array('get'),
                'sf_format' => '(?:json)'
            )
        )
    )
);

This would allow for the configuration of multiple routes for a single endpoint. The above configuration would match

  • /operationcenter/1
  • /operationcenter.json/id/1
  • /operationcenter.json?id=1
  • /operationcenter?id=1

It will NOT match

  • /operationcenter/id/1

This might be useful to allow direct access to a specific resource without specifying the parameter name 'id'. Some clients might require this.

Helpers

There are a few helper classes that provide common and frequently needed methods.

The following classes can be found in lib/REST/Helpers.

arrayHelper

Useful for checking models that are passed into an endpoint to validate required and optional fields. Source

docHelper

Various utility methods for generating nicer docs in the description. Source

encodingHelper

Several methods to ensure strings, arrays and arrays of arrays contain UTF8 encoded strings. Source

jsonHelper

Json encoding and decoding methods with error checking. Used internally and available via Source

<?php
$json = $this->jsonEncode($data);

$data = $this->jsonDecode($json);

methodHelper

For doc generation only

paramHelper

Convenience functions to construct frequently used objects, such as validators. Source

sqlHelper

Methods for building SQL strings. Source

Sql

Whenever possible use Doctrine or PDO prepared statements to access the database.

Doctrine

Pros

  • Fast implementation
  • Easy maintenance
  • Save - it will sanitize all user input for you

Cons

  • Potentially miserable performance - Use it wisely
  • Hard or impossible to implement complex queries (joins or subqueries)

PDO prepared statements

Pros

  • Better performance than Doctrine
  • If using prepared statements user input is sanitized
  • Flexible
  • Simpler to implement complex queries than Doctrine

Cons

  • Potentially dangerous if hand writing SQL and not using prepared statements

Result Schema

What is the ResultSchema?

A mini framework for constructing a schema representation of the API. It provides some convenient methods for pulling in multiple database table layouts, manipulating these for display and for adding fields manually.

Use Case

For any given API method provide a standard way of returning the schema of the result in order to allow third party applications to automatically discover the schema.

Example using automatic table schema discovery

In the API method AniSummaryGET provide a parameter called resultschema that causes the method to return the result schema.

Since this API pulls its data from two different tables, we can let the ResultSchema discover these (fromTable). Of all the fields in these two tables, show only 4 of them and rename them in the output.

Inside a method to construct the ResultSchema:

<?php
$conn = $this->getDoctrineConnection('callcenter');
$rs = new ResultSchema();
$rs
  ->fromTable($conn, 'lcc_contact')
  ->fromTable($conn, 'lcc_contact_extended')
  ->show('lcc_contact_contact_id')->renameTo('id')
  ->show('lcc_contact_fname')->renameTo('first_name')
  ->show('lcc_contact_lname')->renameTo('last_name')
  ->show('lcc_contact_prim_phone')->renameTo('phone');

return $rs->getSchema();

When calling api.laserspineinstitute.com/callcenter/ani/summary.json/resultschema/yes the following will be returned based on the configuration show earlier.

{
    "schema": [
        {
            "name": "id",
            "type": "integer",
            "is-primary": true,
            "index-type": "",
            "length": 4,
            "notnull": true,
            "autoincrement": true
        },
        {
            "name": "first_name",
            "type": "string",
            "is-primary": false,
            "index-type": "",
            "length": 50,
            "notnull": false,
            "autoincrement": false
        },
        {
            "name": "last_name",
            "type": "string",
            "is-primary": false,
            "index-type": "INDEX",
            "length": 50,
            "notnull": false,
            "autoincrement": false
        },
        {
            "name": "phone",
            "type": "string",
            "is-primary": false,
            "index-type": "",
            "length": 30,
            "notnull": false,
            "autoincrement": false
        }
    ]
}

Example of manually adding fields

If there is no source table because the result is computed or if it is simpler to build the schema manually, it is also possible to simply add fields to the schema. The parameters passed to the Field constructor are validated.

<?php
return new ResultSchema()
  ->addField(
    new Field(
      'id',
      'integer',
      true,
      null,
      array(
        'autoincrement' => true,
        'notnull' => true
      )
    )
  )
  ->addField(
    new Field(
      'name',
      'string',
      false,
      'INDEX',
      array(
        'autoincrement' => false,
        'length' => 50
      )
    )
  )
  ->getSchema();

Will result in the following:

{
    "schema": [
        {
            "name": "id",
            "type": "integer",
            "is-primary": true,
            "index-type": "",
            "notnull": true,
            "autoincrement": true
        },
        {
            "name": "name",
            "type": "string",
            "is-primary": false,
            "index-type": "INDEX",
            "length": 50,
            "autoincrement": false
        }
    ]
}

Database Configuration

The api application uses many database connections which are configured in a yml file located at config/databases.yml. This configuration file is tracked by git. Currently the configuration file contains a specific block for each developer/environment that is identified by a top level key (no spaces from left margin) that is the hostname of the machine. The application knows to read the correct configuration block into cache.

On Monday March 9th 2015 this changed!

The config/databases.yml file is no longer under version control. Each developer must maintain their own copy locally. Before pulling down the latest code you must follow the steps in the next section.

Upgrading from previous versions

*** BEFORE PULLING THE LATEST CHANGES FROM GITHUB ***

*** BEFORE PULLING THE LATEST CHANGES FROM GITHUB ***

*** BEFORE PULLING THE LATEST CHANGES FROM GITHUB ***

  1. Copy the config/databases.yml file to a location outside of the project
  2. In the project folder $ git pull origin master
  3. Copy the file from step 1 back into the project folder under config
  4. Edit the file and remove all but your own configuration block
  5. Rename the top level key to all

before

  :
HGGMBP.local:
  api:
    class: sfDoctrineDatabase
    param:
      :

after

all:
  api:
    class: sfDoctrineDatabase
    param:
      :

* ':' alone on a line is supposed to indicate lines that have been omitted

First time setup

  1. Copy the file config/databases.yml.dist to config/databases.yml
  2. Edit the file and fill in your host and password information

Composer Packages

When adding packages make sure that only packages with version numbers are added. If this cannot be done follow the steps 1-4 at the bottom of the page.

For this project any dependencies MUST be committed to the repository. When committing make sure that all files are added. One last check to perform is to go to github and make sure all files are present.

Common Issues

Getting the connection from a table instance

This code

<?php
$conn = MyTable::getInstance()->getConnection()->getDbh();

should work, BUT DOES NOT!!!

There is a bug in Doctrine that will return the default connection for this.

Instead get the connection by its name:

<?php
$conn = Doctrine_Manager::getInstance()->getConnection($name)->getDbh();

//or

$conn = $this->getDoctrineConnection($name)->getDbh();

JSONP

The api server dictates that the name of the parameter that is used to pass the name of the callback function is called jsonp_callback. jQuery is instructed to do this by setting the jsonp parameter to this value. The only other property that needs to be set is dataType, which must be jsonp. It is ok to set the value for jsonpCallback, but it is not necessary.

jQuery.ajax({
    type: 'get',
    url: url,
    dataType: 'jsonp',
    jsonp: 'jsonp_callback',
    success: function() {
    },
    error:function(e){
        console.log(e)
    }
});

jQuery will now generate a url that looks like this:

http://whateverurlyoudefined?jsonp_callback=random

Where random is actually a random string that has no significance for the server side, it is simply sent back as is.