Building an API - Part 2

Published: 3 months ago web dev

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_encoded 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.