Build simple REST API with PHP – Part 3

This is the last part of our post serie about building simple REST API with PHP. We can find the two previous ones here:

  1. Part 1
  2. Part 2

In the last part we’ll add Middlewares to add more features to our API and also customize some error outputs to make them more consumable.

 

Middlewares

We will add the following three middlewares:

  • ContentTypes: Parses JSON-formatted body from the client (the innermost);
  • JSON: utility middleware for “JSON only responses” and “JSON encoded bodies”;
  • Authentication (outermost).

 

ContentType Middleware

For the ContentType middleware we’ll use an existing Slim middleware, so we just need to add it in our bootstrap.php file:

1
2
3
4
...
// Middlewares
// Content Type (inner)
$app->add(new \Slim\Middleware\ContentTypes());

This middleware intercepts the HTTP request body and parses it into the appropriate PHP data structure if possible; else it returns the HTTP request body unchanged. This is particularly useful for preparing the HTTP request body for an XML or JSON API.

 

JSON Middleware

Our JSON middleware achieves two best practices: “JSON only responses” and “JSON encoded bodies”. So, lets add it in our bootstrap.php file:

1
$app->add(new API\Middleware\JSON());

As we can see if we review a classic Middleware class, the call() method of a Slim middleware is where the action takes place. Here is our JSON middleware class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
namespace API\Middleware;

class JSON extends \Slim\Middleware
{
    public function call()
    {
        try {
            // Force response headers to JSON
            $this->app->response->headers->set('Content-Type', 'application/json');

            $method = strtolower($this->app->request->getMethod());
            $mediaType = $this->app->request->getMediaType();
            $body = $this->app->request()->getBody();
            // Validate JSON format
            if (!$this->app->isJSON($body)) {
                throw new \Exception("JSON data is malformed", 400);
            }
            if (in_array($method, array('post', 'put', 'patch')) && '' !== $body) {
                if (empty($mediaType) || $mediaType !== 'application/json') {
                    $this->app->halt(415);
                }
            }
        } catch (\Exception $e) {
            echo json_encode(array(
                    'code' => $e->getCode(),
                    'message' => $e->getMessage()
                ),
                JSON_PRETTY_PRINT
            );
            exit;
        }
    }

    $this->next->call();
}

If the request method is one of the write-enabled ones (PUT, POST, PATCH) the request content type header must be application/json, if not the application exits with a 415 Unsupported Media Type HTTP status code. If all is right the statement $this->next->call() runs the next middleware in the chain.

 

Authentication Middleware

To authenticate our api users we’ll use a third-party library: JWT Authentication Middleware for Slim

1
2
3
$app->add(new \Slim\Middleware\JwtAuthentication([
    "secret" => "supersecretkeyyoushouldnotcommittogithub"
]));

This middleware implements JSON Web Token Authentication for Slim Framework. It does not implement OAuth 2.0 authorization server nor does it provide ways to generate, issue or store authentication tokens. It only parses and authenticates a token when passed via header or cookie.

 

Pretty outputs for Errors

Our API should show useful error messages in pretty format. We need a minimal payload that contains an error code and message. With Slim we can redefine both 404 errors and server errors with the $app->notFound() and $app->error() method respectively.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$app->notFound(function () use ($app) {

    $mediaType = $app->request->getMediaType();
    $isAPI = (bool) preg_match('|^/api/v.*$|', $app->request->getPath());

    if ('application/json' === $mediaType || true === $isAPI) {

        $app->response->headers->set('Content-Type', 'application/json');

        echo json_encode(
            array(
                'code' => 404,
                'message' => 'Not found'
            )
        );

    }
});

For other errors we can use and customize the error() method:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$app->error(function (\Exception $e) use ($app, $log) {

    $mediaType = $app->request->getMediaType();
    $isAPI = (bool) preg_match('|^/api/v.*$|', $app->request->getPath());

    // Standard exception data
    $error = array(
        'code' => $e->getCode(),
        'message' => $e->getMessage()
    );

    // Custom error data (e.g. Validations)
    if (method_exists($e, 'getData')) {
        $errors = $e->getData();
    }
   
    if (!empty($errors)) {
        $error['errors'] = $errors;
    }

    $log->error($e->getMessage());
    if ('application/json' === $mediaType || true === $isAPI) {
        $app->response->headers->set('Content-Type', 'application/json');
        echo json_encode($error);
    }
});

The $app->error() method receives the thrown exception as argument and print it in a pretty way.

Well that’s, we’ve developed a basic API with common best practices. I hope these post series are helpful for you guys.

Build simple REST API with PHP – Part 2

In our first part of this post series we saw how to bootstrap our API and we defined the end points for it. Now, we’ll add the necessary logic to make our end points to work.

End Points

GET /users

Here we will get ALL our existing users.

1
2
3
4
5
6
7
8
9
10
11
12
13
// GET route for all users
$app->get('/users', function () use ($app, $log) {
    $users = array();
    $results = \ORM::forTable('user');

    $users = $results->findArray();

    // Here, we can add sorting, filtering and/or searching logic

    $output = array('code' => 200, 'data' => $users);
    echo json_encode($output, JSON_PRETTY_PRINT);
    return;
});

 

GET /users/:id

Given a user ID we return that user if exists, if not we return a 404 error.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// GET route
$app->get('/users/:id', function ($id) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $user = $user->asArray();
           
        $output = array('code' => 200, 'data' => array($user));
        echo json_encode($output, JSON_PRETTY_PRINT);
        return;
    }
   
    $app->notFound();
});

Observe how the “:id” url parameter is passed as an argument of the callable function.

 

POST /users

Now, sending POST data to the /users end point we will be able to create a new User.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// POST route, for creating
$app->post('/users', function () use ($app, $log) {
    // Get POSTed data in body
    $body = $app->request()->getBody();

    // Validate data here

    $user = \ORM::for_table('user')->create();
    $user->set($body);
    if ($user->save() === true) {
        $output = array(
            'code' => 200,
            'data' => array($user->asArray())
        );
    echo json_encode($output, JSON_PRETTY_PRINT);
        return;
    } else {
        throw new \Exception('Something went wrong.');
    }
});

 

PUT /users/:id

Update an existing user with passed data given a user ID.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// PUT route, for updating
$app->put('/users/:id', function ($id) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $body = $app->request()->getBody();

        // Validate data here

        $user->set($body);
        if ($user->save() === true) {
            $user = $user->asArray();
           
            $output = array('code' => 200, 'data' => array($user));
            echo json_encode($output, JSON_PRETTY_PRINT);
            return;
        } else {
            throw new \Exception('Something went wrong.');
        }
    }
   
    $app->notFound();
});

 

DELETE /users/:id

Here given a user ID we will delete the user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// DELETE route
$app->delete('/users/:id', function ($id) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $user->delete();
        if ($user->save() === true) {
            $output = array('code' => 200, 'data' => array());
            echo json_encode($output, JSON_PRETTY_PRINT);
            return;
        } else {
            throw new \Exception('Something went wrong.');
        }
    }
   
    $app->notFound();
});

 

User’s Phonenumbers

Now, we will define the end points for our second collection Phonenumbers.

GET /users/:id/phonenumbers

In this end point we will return all the associated phonenumbers to a user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// GET route for all users's phonenumbers
$app->get('/users/:id/phonenumbers', function ($id) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $data = $user->asArray();
        $phonenumbers = \ORM::forTable('phonenumber')
            ->where('user_id', $id)
            ->orderByDesc('id')
            ->findArray();
        $data['phonenumbers'] = $phonenumbers;
        $output = array('code' => 200, 'data' => $data);
        echo json_encode($output, JSON_PRETTY_PRINT);
        return;
    }
   
    $app->notFound();
});

 

GET /users/:id/phonenumbers/:pid

Now given a user ID and a phone ID we will return that specific phonenumber for the given user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// GET route for specific user's phonenumber
$app->get('/users/:id/phonenumbers/:pid', function ($id, $phoneId) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $data = $user->asArray();
        $phonenumber = \ORM::forTable('phonenumber')
            ->where('user_id', $id)
            ->andWhere('id', $phoneId)
            ->findOne();
        if ($phonenumber) {
            $data['phonenumber'] = $phonenumber;
            $output = array('code' => 200, 'data' => $data);
            echo json_encode($output, JSON_PRETTY_PRINT);
            return;
        }
    }
   
    $app->notFound();
});

 

POST /users/:id/phonenumbers

With this end point we will be able to create/add a phonenumber to a user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// POST route, for creating a new user's phonenumber
$app->post('/users/:id/phonenumbers', function ($id) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $data = $user->asArray();
        // Get POSTed data in body
        $body = $app->request()->getBody();

        // Validate data here

        $phonenumber = \ORM::for_table('phonenumber')->create();
        $phonenumber->set($body);
        if ($phonenumber->save() === true) {
            $data['phonenumber'] = $phonenumber->asArray();
            $output = array(
                'code' => 200,
                'data' => $data
            );
        echo json_encode($output, JSON_PRETTY_PRINT);
            return;
        } else {
            throw new \Exception('Something went wrong.');
        }
    }
   
    $app->notFound();
});

 

PUT /users/:id/phonenumbers/:pid

PUT route to update a user’s phonenumber.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// PUT route, for updating a user's phonenumber
$app->put('/users/:id/phonenumbers/:pid', function ($id, $phoneId) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $data = $user->asArray();
        $phonenumber = \ORM::forTable('phonenumber')
            ->findOne($phoneId);

        if ($phonenumber) {
            $data = $user->asArray();
            // Get POSTed data in body
            $body = $app->request()->getBody();

            // Validate data here

            $phonenumber->set($body);
            if ($phonenumber->save() === true) {
                $data['phonenumber'] = $phonenumber->asArray();
                $output = array(
                    'code' => 200,
                    'data' => $data
                );
            echo json_encode($output, JSON_PRETTY_PRINT);
                return;
            } else {
                throw new \Exception('Something went wrong.');
            }
        }
    }
   
    $app->notFound();
});

 

DELETE /users/:id/phonenumbers/:pid

And at the end, we define the route to delete an specific user’s phonenumber.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// DELETE route, for delete a user's phonenumber
$app->delete('/users/:id/phonenumbers/:pid', function ($id, $phoneId) use ($app, $log) {
    $user = \ORM::forTable('user')
        ->findOne($id);

    if ($user) {
        $phonenumber = \ORM::forTable('phonenumber')
            ->findOne($phoneId);
        if ($phonenumber) {
            $phonenumber->delete();
            if ($phonenumber->save() === true) {
                $output = array('code' => 200, 'data' => array());
                echo json_encode($output, JSON_PRETTY_PRINT);
                return;
            } else {
                throw new \Exception('Something went wrong.');
            }
        }
    }
   
    $app->notFound();
});

 

Well, we finished defining all our end points to interect against our API. At this point we have a basic API working but still there are thing to do to make it more fully functional. For instance, what if we want to make our API secure? We should add authentication, and we’ll do it using really great Slim’s functionality Middlewares. Also, we can ensure all our requests/responses are being talking JSON and/or add some rate limit to don’t allow overwhelming our API.

In the next and last post we will cover all this. See you soon! :D.