A Simple Slim REST API Service

In a recent post, I showed how to write an AngularJS CRUD application using Firebase as the backend. In this post, we’ll be setting up our own REST service using the PHP microframework Slim.

Couple of introductory notes: this application will require you to have a server and a database. There are plenty of options out there, but this post will assume you know how to set that up. If you need a starting point for this, I’d recommend xampp which ships with phpmyadmin for interacting with the databse. That said, here’s the beginning of our file structure. If you’re using xampp, put phpang in your htdocs. Then you can go to localhost/phpang in your browser to view this application.

phpang
|
+--api
|  \--index.php
|  \--composer.json
|  +--db
|    \config.php
+-- css
|  \-- bootstrap.min.css
|  \-- starter-template.css
+-- js
|  \-- app.js
|  +-- controllers
|    \-- ListController.js
|    \-- AddController.js
|    \-- EditController.js
+-- views
|   \-- add.html
|   \-- edit.html
|   \-- list.html
+-- index.html

Moving on, let’s take care of our dependencies. Put this in your composer.json.

{
    "require": {
        "slim/slim": "2.*",
        "vrana/notorm": "dev-master"
    },
    "autoload": {
        "classmap": [
            "vendor/vrana/notorm/"
        ]
    }
}

With this file in place, open your terminal in phpang/api and enter composer install. You should see a new folder under api called vendor with a bunch of files and directories. You’ve just installed Slim and NotORM, which we’ll be using to interact with the database. You also set up an autoloader classmap for both services.

There is a tool that is really useful when building APIs in the Chrome Web Store called Advanced REST client. We’ll be using that to test our API service as we go along.

Next, we’ll set up our database. Create a new database, which we’ll call phpang, then import this schema. This just sets up a table called products and puts in a few rows of data. Now we need to fill in config.php with this:

<?php
//DB Params
define("DSN", "mysql:host=localhost;dbname=YOUR_DB_NAME");
define("DB_USER", "YOUR_USERNAME");
define("DB_PASS", "YOUR_PASSWORD");

As you probably already noticed, you’ll need to adjust that to fit the name and credentials of your database.

Now that we have our file structure in place, our dependencies installed and some test data in our database, we’re ready to start building our API.

Open up api/index.php in your text editor. These first few lines will put all the work we just did to use:

<?php

/**
 * requires 
 */

require 'vendor/autoload.php';
require 'db/config.php';

/**
 * db stuff
 */

$pdo = new PDO(DSN, DB_USER, DB_PASS);
$db = new NotORM($pdo);

/**
 * Instantiate a Slim application
 */

$app = new \Slim\Slim();

$app->run();

The requires bring NotORM, Slim and our database credentials into our app. Then we set up a couple variables which we’ll be using frequently to get information in and out of our database. Next we officially instantiate a Slim application.

Quick background on what we’ll be doing next. In my previous post, we set up a CRUD application. The API equivalent verbs to Create, Read, Update and Delete are GET, PUT, POST, DELETE. The first thing we’ll be doing is setting up GET to retrieve all our products from the database. Put all the rest of the code before the line $app->run();.

// Route for GET on all products
$app->get(
    '/products',
    function () use ($app, $db) {
        $products = array();
        foreach ($db->products() as $product){
            $products[] = [
              'id' => $product['id'],
              'sku' => $product['sku'],
              'description' => $product['description'],
              'price' => $product['price']
              ];
        }
        $app->response()->header(
            "Content-Type", "application/json");
        echo json_encode($products, JSON_FORCE_OBJECT);
    }
);

These lines of code set up the route localhost/phpang/api/products. When a REST client sends a GET request to that URL, a response is sent back in JSON with the data from our database. In our next post, I’ll go through refactoring the code from our AngularJS CRUD application to use this API, but for now, we’ll just be using the Advanced REST client from the Chrome Webstore. In that app, there’s a place to enter the url that the client should be hitting. In our case, that’s http://localhost/phpang/api/products. Select the GET radio button and hit send. It should look something like this:

GET

As you can see, when your client sends a GET request to localhost/phpang/api/products, your service responds with JSON object of all the products in your database.

Next, let’s set up a route for getting a single product.

// Route for GET on product by id
$app->get(
    '/products/:id',
    function ($id) use ($app, $db) {
        $app->response()->header("Content-Type", "application/json");
        $product = $db->products()->where("id", $id);
        if ($product[$id]) {
            echo json_encode($product[$id], JSON_FORCE_OBJECT);
        } else {
            echo json_encode(array(
                "status" => false,
                "message" => "Product ID $id does not exist."
                ), JSON_FORCE_OBJECT);
       }
    }
);

Now, when you send a get request to products/2, for example, you’ll get back a JSON object of that particular product. This code first checks to make sure it exists in the database, then responds appropriately. To test out the error message, try going to http://localhost/phpang/api/products/88 and you should see:

FALSE

This looked for a product with id 88 and didn’t find it so it gave us this handy little error message.

We don’t have a whole lot of products in our database so let’s set up a way to add them through an API. Here’s our POST route:

// POST route
$app->post(
    '/products/post',
    function () use($app, $db) {
        $app->response()->header("Content-Type", "application/json");
        $post = $app->request()->post();
        $body = $app->request->getBody();
        $result = $db->products->insert(json_decode($body, true));
        echo json_encode($body);
    }
);

This allows us to send a JSON object to products/post which is then entered in our database. Test it out and make sure you see this:

POST

You should be able to send a GET request to /products and see your new product in the list.

The PUT route allows you to update products.

// PUT route
$app->put(
    '/products/:id',
    function ($id) use($app, $db) {
      $app->response()->header("Content-Type", "application/json");
      $product = $db->products()->where("id", $id);
      if ($product[$id]) {
        $post = $app->request()->put();
        $body = $app->request->getBody();
        $result = $product->update(json_decode($body, true));
        echo json_encode(array(
          "status" => (bool)$result,
          "message" => "Product updated successfully."), 
            JSON_FORCE_OBJECT);
      } else {
        echo json_encode(array(
          "status" => false,
          "message" => "Product ID $id does not exist."),               
              JSON_FORCE_OBJECT);
      }
    }
);

By now, this code should be looking pretty familiar. If the product exists in the database, we update it with the new information. If not, we return a message saying it doesn’t exist.

Finally, we have our DELETE functionality to put in place.

// DELETE route
$app->delete(
    '/products/:id',
    function ($id) use ($app, $db) {
        $app->response()->header("Content-Type", "application/json");
        $product = $db->products()->where("id", $id);
        if ($product[$id]) {
          $result = $product->delete();
          echo json_encode(array(
            "status" => true,
            "message" => "Product deleted successfully."), 
              JSON_FORCE_OBJECT);
        } else {
          echo json_encode(array(
            "status" => false,
            "message" => "Delete failed. Product ID $id does not exist."),
              JSON_FORCE_OBJECT);
        }
    }
);

You now have a REST service that can handle GET, PUT, POST, and DELETE requests. You can clone working copy of the code here. You’ll also find an AngularJS front-end which was modified from my last blog post to work with this API.