Building an API - Part 2
It's the second and final part of how to build a simple API in PHP. To recap, part 1 covered setting up the basics and using the Symfony Routing component while the additional part 1.5 covered using the Symfony dotenv component to store our API's config.
In this part we look at actually mapping routes to controllers that actually do something! But first, we are going to store some data in a database so we need to connect to MySQL.
Connecting to MySQL
We're going to use the PDO class to communicate with MySQL so whilst we will create a Database class, it's really going to be a simple wrapper to PDO. Here it is:
<?php
namespace App;
class Database
{
private $pdo;
public function __construct($dbConnectionString,$dbUser,$dbPass){
$this->pdo = new \PDO($dbConnectionString,$dbUser,$dbPass);
}
public function getDatabase(){
return $this->pdo;
}
}
Now we just need to instantiate it in our App class constructor:
$this->database = new Database('mysql:host='.getenv('DBHOST').';dbname='.getenv('DBNAME').'',''.getenv('DBUSER').'',''.getenv('DBPASS'));
And create a private variable:
private $database;
And create a getter:
public function getDatabase()
{
return $this->database->getDatabase(); //return the PDO object
}
Now our database is available to App
anywhere by calling $this->database
. The database credentials are of course loaded in from environmental variables or are in our .env
file for testing. I'll show an example for querying the database at the end.
Handling requests
In part 1, when a route matched we just printed 'The route matched'. But let's look at an object orientated way of handling matching routes. Our end goal here is to have individual controllers that can be defined in routes.php
and return a response.
In handleRequest()
in App
, change the try
block to:
$match = $this->getRouter()->match( $this->getRequest()->getPathInfo() );
$ns = '\App\Calls\\' . $match['_controller'];
echo (new $ns)->response( $match, $this );
Here we are creating a variable to hold a object's namespace, the namespace itself is dynamically generated based on the _controller
parameter supplied by the router. Once we have the that namespace, we create an instance of that object and echo out the response()
method passing in the matched data and (importantly) the App
object itself.
So now we can see what we need to actually return a response. We need an object that lives in ./app/Calls
that matches the name of the _controller
attribute from router
. We know that we are going to need all objects that live in the Calls
directory to return a response($routerData, $app)
method so lets create an interface to enforce that first:
In ./app/Interfaces
create ApiCallInterface.php
with the following code:
<?php
namespace App\Interfaces;
interface ApiCallInterface
{
public function response($params, $appInstance);
}
Now we can create the actual object to generate a response. If we go back to part 1, you can see our routes.php
specifies a matched route of any type returns Stats
for the _controller
parameter. So we need to create a Stats
object in Calls
that implements the interface we just created:
<?php
namespace App\Calls;
use App\Interfaces\ApiCallInterface;
class Stats implements ApiCallInterface
{
public function response($params, $appInstance)
{
return json_encode('it works');
}
}
Now when anyone goes to /stats
on our API, it will return a json_encode
d response as above. Well if you've followed by rambling guide it will. That's basically it! We can now see how we might want to query data and use the App
object from within a Call
object:
$select = $appInstance->getDatabase()->prepare("SELECT * FROM stats");
$select->execute();
$selectResult = $select->fetchAll(\PDO::FETCH_COLUMN|\PDO::FETCH_GROUP);
if($selectResult){
return json_encode($selectResult);
} else {
$appInstance->getResponse()->setStatusCode(404);
$appInstance->getResponse()->setContent('Not found');
$appInstance->getResponse()->send();
}
Our App
object is available through $appInstance
so we can make calls to the database directly (using PDO methods) and we can set the response directly from this object too.
And we're done. This should provide a useful starting point for creating an API. I may look to write an additional post on testing this API in the future. Stay tuned.