Today we will be looking at creating a RESTful API using ReactPHP, MySQL and nikic/FastRoute. Let’s look at the API we want to build and what it can do.

Application

We are going to build the API that:

  • Handles CRUD operations on a resource (we are going to use users)
  • Uses the proper HTTP verbs to make it RESTful (GET, POST, PUT, and DELETE)
  • Returns JSON data

All of this is pretty standard for RESTful APIs. Feel free to switch out users for anything you want for your application (orders, products, customers, etc).

Getting Started

Here is our file structure. We won’t need many files and we’ll keep this very simple for demonstration purposes.

 - src/             // Contains project files
 - index.php        // Entry point to our application
 - composer.json    // Define our app and its dependencies
 - vendor/          // Created by Composer and contains our dependencies

Installing Our Dependencies

We need to install several packages:

Go to the command line in the root of the project and run the following commands:

$ composer require react/http
$ composer require react/mysql
$ composer require nikic/fast-route

Simple and easy. Then open composer.json and add autoloading section. We are going to autoload classes from src folder into App namespace:

{
    "require": {
        "react/mysql": "^0.5",
        "react/http": "^0.8",
        "nikic/fast-route": "^1.3"
    },
    "autoload": {
        "psr-4": {
            "App\": "src/"
        }
    }
}

Now that we have our packages being installed, let’s go ahead and use them to set up our API.

Setting Up Our Server

First of all, we need a running server that will handle incoming requests. Create an empty HTTP server:

<?php

use React\Http\Response;
use React\Http\Server;
use React\MySQL\Factory;

$loop = \React\EventLoop\Factory::create();

$hello = function () {
    return new Response(200, ['Content-type' => 'text/plain'], 'Hello');
};

$server = new Server($hello);
$socket = new \React\Socket\Server('127.0.0.1:8000', $loop);
$server->listen($socket);

echo 'Listening on ' . str_replace('tcp:', 'http:', $socket->getAddress()) . PHP_EOL;
$loop->run();

This is our entry point, our future RESTful API server. Here we have a “hello-world” HTTP server. It has one middleware $hello which is triggered for all incoming requests and returns a plain text string ‘Hello’.

If you are not familiar with ReactPHP middleware and don’t know how they work check this article.

Let’s make sure that everything is working up to this point. Start the server. From the command line, type:

$ php index.php
Listening on http://127.0.0.1:8000

You should see the message saying that the server is up and is listening. Now that we know our application is up and running, let’s test it. Make a GET request to http://127.0.0.1:8000.

Nice! We got back exactly what we wanted. Now, let’s wire up our database so we can start performing CRUD operations on users.

Database

Create a database and table users with the following schema: idname, and email. Email field is unique.

CREATE TABLE users
(
    id INT(11) UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    email VARCHAR(255) NOT NULL,
    CONSTRAINT users_email_uindex UNIQUE (email)
);

Now, let’s connect to a database. Create a factory React\MySQL\Factory. Then ask it for a lazy connection, and provide a connection string. My connection string consists of: username root, has no password, host localhost, and database called “test”.

<?php

$factory = new Factory($loop);
$db = $factory->createLazyConnection('root:@localhost/test');

The main idea of lazy connection is that internally it lazily creates the underlying database connection only on demand once the first request is invoked and it will queue all outstanding requests until the underlying connection is ready.

Creating Basic Routes

We will now create the routes to handle getting all the users and creating a new user. Both routes will be handled using the /users path.

Getting All Users

GET /users

Create a middleware $listUsers and pass an instance of the connection inside. Now, we are ready to execute queries. Select all users and return them as a JSON object:

$listUsers = function () use ($db) {
    return $db->query('SELECT id, name, email FROM users ORDER BY id')
        ->then(function (\React\MySQL\QueryResult $queryResult) {
            $users = json_encode($queryResult->resultRows);

            return new Response(200, ['Content-type' => 'application/json'], $users);
        });
};

$server = new Server($listUsers);

Method query() accepts a raw SQL string and returns a promise that resolves with an instance of QueryResult. To grab resulting rows we use resultRows property of this object. It will be an array of arrays, that represent the result of the query. Then convert them to JSON and return with an corresponding Content-type header. I have also changed the middlware in the Server constructor from $hello to $listUsers.

Check our API, make a GET request to http://127.0.0.1:8000 and you will receive an empty list. Add several users to the database and check again. Now it should return them.

MySQL query works, but the server is still a sort of “hello-world” one. It responds the same way to all incoming requests. It’s time to fix it and add routing to our application.

Create class Router in src folder:

<?php

namespace App;

use FastRoute\Dispatcher;
use FastRoute\Dispatcher\GroupCountBased;
use FastRoute\RouteCollector;
use LogicException;
use Psr\Http\Message\ServerRequestInterface;
use React\Http\Response;

final class Router
{
    private $dispatcher;

    public function __construct(RouteCollector $routes)
    {
        $this->dispatcher = new GroupCountBased($routes->getData());
    }

    public function __invoke(ServerRequestInterface $request)
    {
        $routeInfo = $this->dispatcher->dispatch($request->getMethod(), $request->getUri()->getPath());

        switch ($routeInfo[0]) {
            case Dispatcher::NOT_FOUND:
                return new Response(404, ['Content-Type' => 'text/plain'], 'Not found');
            case Dispatcher::METHOD_NOT_ALLOWED:
                return new Response(405, ['Content-Type' => 'text/plain'], 'Method not allowed');
            case Dispatcher::FOUND:
                $params = $routeInfo[2];
                return $routeInfo[1]($request, ... array_values($params));
        }

        throw new LogicException('Something wrong with routing');
    }
}

It is a middleware-wrapper on top of FastRoute.

I’m not going to cover details of using FastRoute in ReactPHP project. Instead, we will focus on writing controllers and database quires. We are using here a router from one of the previous articles: Using Router With ReactPHP Http Component.

This middleware encapsulates a collection of routes and inside we check method and path of the request. $routeInfo[0] contains the result of the matching. If the request matches one of the defined routes we execute a corresponding controller with a request object and matched params (if they were defined). Otherwise, we return 404 or 405 responses.

Then define the routes. For example, this $listUsers middleware responds only to GET requests to path /users.

<?php

// ...

use FastRoute\DataGenerator\GroupCountBased;
use FastRoute\RouteCollector;
use FastRoute\RouteParser\Std;

// ...

$routes = new RouteCollector(new Std(), new GroupCountBased());
$routes->get('/users', $listUsers);

We instantiate an instance of FastRoute\RouteCollector with required dependencies and add our first route for GET request. Then we pass the router to the server:

<?php

// ...

$server = new Server(new \App\Router($routes));

The first endpoint of our API is ready. In response to GET request to /users path, we return a JSON representation of users.

Create a New User

POST /users

Create a new middleware (controller) $createUser. For this endpoint, we use the same path /users but the request method will be POST:

<?php

$createUser = function (ServerRequestInterface $request) use ($db) {
    // ...  
};

$routes->get('/users', $listUsers);
$routes->post('/users', $createUser);

Assume that we receive user data in JSON. So, we get the response body and decode it to an array:

<?php

$createUser = function (ServerRequestInterface $request) use ($db) {
    $user = json_decode((string) $request->getBody(), true);

    // ...
};

Now we are ready to insert $user array into users table. Again call $db->query() and provide a raw SQL query and an array of data to be inserted:

<?php

$createUser = function (ServerRequestInterface $request) use ($db) {
    $user = json_decode((string) $request->getBody(), true);

    return $db->query('INSERT INTO users(name, email) VALUES (?, ?)', $user)
        ->then(function () { return new Response(201); });
};

Once the request is done we return 201 response.

I have skipped validation here on purpose, to make examples easier to understand. In a real life, you should never trust input data.

It looks like we are performing raw queries and everything we pass inside query() method will be placed right into the query. So, it looks like there is a room for a SQL injection. Don’t worry, when we execute a query all provided params are escaped. So, feel free to provide any values and don’t be afraid of SQL injection. OK, the endpoint is done. Let’s check it. Create a new user:

Then request all users.

We see that a new user has been stored in the database. Let’s try to add the same user one more time.

It returns 500 response. Seems like the query has failed but there is no actual error anywhere. To view the error we can add a rejection handler to our query and return an error message as a response:

<?php

return $db->query('INSERT INTO users(name, email) VALUES (?, ?)', $user)
    ->then(
        function () {
            return new Response(201);
        },
        function (Exception $error) {
            return new Response(
                400, ['Content-Type' => 'application/json'], json_encode(['error' => $error->getMessage()])
            );
        }
    );

Then restart the server and execute the request. Now the problem is clear. We receive a clear “bad request” response, explaining that we are trying to insert the user with a duplicated email.

Here is the complete code of $createUser controller:

<?php

$createUser = function (ServerRequestInterface $request) use ($db) {
    $user = json_decode((string) $request->getBody(), true);

    return $db->query('INSERT INTO users(name, email) VALUES (?, ?)', $user)
        ->then(
            function () {
                return new Response(201);
            },
            function (Exception $error) {
                return new Response(
                    400, ['Content-Type' => 'application/json'], json_encode(['error' => $error->getMessage()])
                );
            }
        );
};

Refactoring

Our application is becoming messy. Adding more controllers will only make things worse. Let’s replace our procedural code with classes. Create folder src/Controller. We are going to store controller classes here.

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

I assume that you have a root namespace App and autoloading in your composer.json file.

At first we extract $listUsers controller. Move the whole logic to its own class App\Controller\ListUsers:

<?php

namespace App\Controller;

use Psr\Http\Message\ServerRequestInterface;
use React\Http\Response;
use React\MySQL\ConnectionInterface;

final class ListUsers
{
    private $db;

    public function __construct(ConnectionInterface $db)
    {
        $this->db = $db;
    }

    public function __invoke(ServerRequestInterface $request)
    {
        return $this->db->query('SELECT id, name, email FROM users ORDER BY id')
            ->then(function (\React\MySQL\QueryResult $queryResult) {
                $users = json_encode($queryResult->resultRows);

                return new Response(200, ['Content-type' => 'application/json'], $users);
            }
        );
    }
}

Notice that a database connection is now injected into the constructor. The rest of the code stays the same. Having raw SQL queries in controllers is not a good practice. We can extract them out and create a sort of repository for users. Create class Users in src folder:

<?php

namespace App;

use React\MySQL\ConnectionInterface;
use React\MySQL\QueryResult;
use React\Promise\PromiseInterface;

final class Users
{
    private $db;

    public function __construct(ConnectionInterface $db)
    {
        $this->db = $db;
    }

    public function all(): PromiseInterface
    {
        return $this->db->query('SELECT id, name, email FROM users ORDER BY id')
            ->then(function (QueryResult $queryResult) {
                return $queryResult->resultRows;
            });
    }
}

It encapsulates a database connection. Method all() returns a promise that resolves with an array that contains raw users data. Now, inside the controller, we inject an instance of Users class instead of MySQL connection. And replace a raw SQL query with a call of Users::all():

<?php

namespace App\Controller;

use App\JsonResponse;
use App\Users;
use Psr\Http\Message\ServerRequestInterface;

final class ListUsers
{
    private $users;

    public function __construct(Users $users)
    {
        $this->users = $users;
    }

    public function __invoke(ServerRequestInterface $request)
    {
        return $this->users->all()
            ->then(function(array $users) {
                return new Response(200, ['Content-type' => 'application/json'], $users);
            });
    }
}

Done. But there is still a room for improvement here. As it was mentioned before our API returns JSON responses. So, instead of repeating the same response building logic we can also create our own custom JsonResponse class - a wrapper on top of React\Http\Response. It will accept a status code and the data we want to return. JSON encoding logic and required headers will be encapsulated in this class. Create it in src folder:

<?php

namespace App;

use React\Http\Response;

final class JsonResponse extends Response
{
    public function __construct(int $statusCode, $data = null)
    {
        $body = $data ? json_encode($data) : null;

        parent::__construct($statusCode, ['Content-Type' => 'application/json'], $body);
    }
}

Then return an instance of JsonResponse in the controller:

<?php

namespace App\Controller;

use App\JsonResponse;
use App\Users;
use Psr\Http\Message\ServerRequestInterface;

final class ListUsers
{
    private $users;

    public function __construct(Users $users)
    {
        $this->users = $users;
    }

    public function __invoke(ServerRequestInterface $request)
    {
        return $this->users->all()
            ->then(function(array $users) {
                return JsonResponse::ok($users);
            });
    }
}

Done. The controller looks pretty simple and readable. The same way we can move $createUser controller to its own class App\Controller\CreateUser:

<?php

namespace App\Controller;

use App\JsonResponse;
use App\Users;
use Exception;
use Psr\Http\Message\ServerRequestInterface;

final class CreateUser
{
    private $users;

    public function __construct(Users $users)
    {
        $this->users = $users;
    }

    public function __invoke(ServerRequestInterface $request)
    {
        $name = $user['name'] ?? '';
        $email = $user['email'] ?? '';

        return $this->users->create($name, $email)
            ->then(
                function () {
                    return new Response(201);
                },
                function (Exception $error) {
                    return new Response(
                        400, 
                        ['Content-Type' => 'application/json'], 
                        json_encode(['error' => $error->getMessage()])
                    );
                }
            );
    }
}

Update Users class and a missing method create():

<?php

namespace App;

use React\MySQL\ConnectionInterface;
use React\MySQL\QueryResult;
use React\Promise\PromiseInterface;

final class Users
{
    // ... 

    public function create(string $name, string $email): PromiseInterface
    {
        return $this->db->query('INSERT INTO users(name, email) VALUES (?, ?)', [$name, $email]);
    }
}

It returns a promise that resolves when a new record has been added to the database.

Status codes and response structure can be hidden behind JsonResponse class. Let’s add some named constructors to it:

<?php

namespace App;

use React\Http\Response;

final class JsonResponse extends Response
{
   // ..

    public static function created(): self
    {
        return new self(201);
    }

    public static function badRequest(string $error): self
    {
        return new self(400, ['error' => $error]);
    }
}

Then use them in the controller:

<?php

namespace App\Controller;

// ...

final class CreateUser
{
    // ...

    public function __invoke(ServerRequestInterface $request)
    {
        $user = json_decode((string) $request->getBody(), true);
        $name = $user['name'] ?? '';
        $email = $user['email'] ?? '';

        return $this->users->create($name, $email)
            ->then(
                function () {
                    return JsonResponse::created();
                },
                function (Exception $error) {
                    return JsonResponse::badRequest($error->getMessage());
                }
            );
    }
}

This makes the controller even more readable. Then we move back to the main script and replace functions with objects.

<?php

$users = new \App\Users($db)
// ...


$routes->get('/users', new \App\Controller\ListUsers($users));
$routes->post('/users', new \App\Controller\CreateUser($users));

Routes for A Single Item

We’ve handled the group for routes ending in /users. Let’s now handle the routes for when we pass in a parameter like a user’s id.

The things we’ll want to do for this route, which will end with /users/{id} will be:

  • Get a single user.
  • Update a user’s info.
  • Delete a user.

Get a Single User

GET /users/{id}

We start by adding a new method find(string $id) to our Users class:

<?php

namespace App;

use React\MySQL\ConnectionInterface;
use React\MySQL\QueryResult;
use React\Promise\PromiseInterface;

final class Users
{
    // ...

    public function find(string $id): PromiseInterface
    {
        return $this->db->query('SELECT id, name, email FROM users WHERE id = ?', [$id])
            ->then(function (QueryResult $result) {
                if (empty($result->resultRows)) {
                    throw new UserNotFoundError();
                }
                
                return $result->resultRows[0];
            });
    }
}

As you can see I have added a new custom exception UserNotFoundError. The code here is very straightforward. Make a SELECT query and return a promise. If there is no such record in the database we throw an exception and the promise rejects. Otherwise, the promise resolves with an array of a user’s data.

Here is UserNotFoundError class:

<?php

namespace App;

use RuntimeException;

final class UserNotFoundError extends RuntimeException
{
    public function __construct($message = 'User not found')
    {
       parent::__construct($message);
    }
}

Before writing a controller let’s add one more custom response to JsonResponse class. For this endpoint we definitely need a 404 response, so add a new named constructor notFound which accepts a string:

<?php

namespace App;

use React\Http\Response;

final class JsonResponse extends Response
{
    // ...

    public static function notFound(string $error): self
    {
        return new self(404, ['error' => $error]);
    }
}

Now we are ready to create a new controller App\Controller\ViewUser:

<?php

namespace App\Controller;

use App\JsonResponse;
use Psr\Http\Message\ServerRequestInterface;
use React\MySQL\ConnectionInterface;
use React\MySQL\QueryResult;

final class ViewUser
{
    private $users;

    public function __construct(Users $users)
    {
        $this->users = $users;
    }

    public function __invoke(ServerRequestInterface $request, string $id)
    {
        return $this->users->find($id)
            ->then(
                function (array $user) {
                    return JsonResponse::ok($user);
                },
                function (UserNotFoundError $error) {
                    return JsonResponse::notFound($error->getMessage());
                }
            );
    }
}

Here we ask Users object to find a user by its id and return a corresponding response. If the promise was rejected with UserNotFoundError we return a 404 response. Otherwise, we return a JSON representation of a user. Then define a new route:

<?php

$routes->get('/users', new \App\Controller\ListUsers($users));
$routes->post('/users', new \App\Controller\CreateUser($users));
$routes->get('/users/{id}', new \App\Controller\ViewUser($users));

From the call to get all users, we can see id of one of our users. Let’s grab that id and test getting that single user.

We can grab one user from our API now! Let’s look at updating that user’s name.

Update a User’s Name

PUT /users/{id}

Our users have only three possible fields: idemail, and name. We will allow changes only for names. Update Users and add a new method update():

<?php

namespace App;

use React\MySQL\ConnectionInterface;
use React\MySQL\QueryResult;
use React\Promise\PromiseInterface;

final class Users
{
    // ...

    public function update(string $id, string $newName): PromiseInterface
    {
        return $this->find($id)
            ->then(function () use ($id, $newName) {
                $this->db->query('UPDATE users SET name = ? WHERE id = ?', [$newName, $id]);
            });
    }
}

The method accepts an id of the user and a new name. We don’t check for affectedRows property of the QueryResult object because in UPDATE request we cannot detect whether there is no record with such id, or there is no need in updating a field value. So, we need to find a user first. And if there is such record in the database we perform an update. The resulting promise rejects with UserNotFoundError or resolves on successful update.

Then create a new controller App\Controller\UpdateUser:

<?php

namespace App\Controller;

use App\JsonResponse;
use App\UserNotFoundError;
use App\Users;
use Psr\Http\Message\ServerRequestInterface;

final class UpdateUser
{
    private $users;

    public function __construct(Users $users)
    {
        $this->users = $users;
    }

    public function __invoke(ServerRequestInterface $request, string $id)
    {
        $name = $this->extractName($request);
        if (empty($name)) {
            return JsonResponse::badRequest('"name" field is required');
        }

        return $this->users->update($id, $name)
            ->then(
                function () {
                    return JsonResponse::noContent();
                },
                function (UserNotFoundError $error) {
                    return JsonResponse::notFound($error->getMessage());
                }
            );
    }

    private function extractName(ServerRequestInterface $request): ?string
    {
        $params = json_decode((string)$request->getBody(), true);
        return $params['name'] ?? null;
    }
}

Here we extract the name field from the received request body. If it is not present or is empty we return a bad request. Then we try to update a user. If it has been successfully updated and we return 204 response. If the promise rejects we return 404 response. I have already updated JsonResponse class with a new static constructor for 204 status code:

<?php

namespace App;

use React\Http\Response;

final class JsonResponse extends Response
{
    // ...

    public static function noContent(): self
    {
        return new self(204);
    }
}

Add a new route:

<?php

$routes->get('/users', new \App\Controller\ListUsers($users));
$routes->post('/users', new \App\Controller\CreateUser($users));
$routes->get('/users/{id}', new \App\Controller\ViewUser($users));
$routes->put('/users/{id}', new \App\Controller\UpdateUser($users));

Make a PUT request to check that everything works as expected.

We have received 204 status code and a record in the database has changed.

Deleting a User

DELETE /users/{id}

The last endpoint in our tutorial is responsible for deleting a user. Create a new method delete() in Users class:

<?php

namespace App;

use React\MySQL\ConnectionInterface;
use React\MySQL\QueryResult;
use React\Promise\PromiseInterface;

final class Users
{
    // ...

    public function delete(string $id): PromiseInterface
    {
        return $this->db
            ->query('DELETE FROM users WHERE id = ?', [$id])
            ->then(
                function (QueryResult $result) {
                    if ($result->affectedRows === 0) {
                        throw new UserNotFoundError();
                    }
                });
    }
}

Here we check affectedRows property of the QueryResult object to detect whether a user has been deleted or not.

App\Controller\DeleteUser controller looks the following:

<?php

namespace App\Controller;

use App\JsonResponse;
use App\UserNotFoundError;
use App\Users;
use Psr\Http\Message\ServerRequestInterface;

final class DeleteUser
{
    private $users;

    public function __construct(Users $users)
    {
        $this->users = $users;
    }

    public function __invoke(ServerRequestInterface $request, string $id)
    {
        return $this->users->delete($id)
            ->then(
                function () {
                    return JsonResponse::noContent();
                },
                function (UserNotFoundError $error) {
                    return JsonResponse::notFound($error->getMessage());
                }
            );
    }
}

As always define a new route:

<?php

$routes->get('/users', new \App\Controller\ListUsers($users));
$routes->post('/users', new \App\Controller\CreateUser($users));
$routes->get('/users/{id}', new \App\Controller\ViewUser($users));
$routes->put('/users/{id}', new \App\Controller\UpdateUser($users));
$routes->delete('/users/{id}', new \App\Controller\DeleteUser($users));

Now when we send a request to our API using DELETE method with the proper user’s id, we’ll delete this user. For example, let’s delete our first user with id 1.

Conclusion

We now have the means to handle CRUD on a specific resource through our own API. Using the techniques above should be a good foundation to move into building larger and more robust APIs.

This has been a quick look at creating RESTful API with ReactPHP and MySQL. There are many more things you can do. For example, you can add authentication and create validation with better error messages.


Further Reading: in the next articles we will cover different types of authentication that can be used to protect RESTful API endpoints:

You can find examples from this article on GitHub.

This article is a part of the ReactPHP Series.

 

[출처] https://sergeyzhuk.me/2019/02/18/restful-api-with-reactphp-and-mysql/