Creating an API in PHP - Part 1

Published: 6 years ago php web dev

For a project I'm working on, I need to write a simple API in PHP to fetch data. This is part 1 of the setup of the API in which we discuss Composer and the Symfony HttpFoundation and Routing components.

API Requirements

Our API has very simple requirements as far as APIs go. We need to be able to provide a variety of endpoint URLs to access data, be able to read from a MySQL database and return that data in a JSON format. Thus we don't need to worry about POSTing data, writing data, handling authentication, rate limiting or any extra tasks.

API Design

We will keep our API design very simple. My API returns data (the data is not important) for specific dates and can also return overall stats (for all dates), so actually all we need to support that is:

/api/stats - return our generic stats for all dates.

/api/stats/yyyy-mm-dd - return data and stats for a specific date specified in yyyy-mm-dd format.

Composer

Composer is pretty much the de facto standard in PHP dependency management now. Composer is really easy to install so get to it. Once we have composer installed we need to pull in our required third party packages. We need the HttpFoundation component, the Routing component and also the Config component (as that is used within the other components).

To install all 3 it is as simple as running the below in your working directory:

composer require symfony/config symfony/http-foundation symfony/routing.

This will create a vendor directory and composer.json and composer.lock files. In the vendor directory, you'll find autoload.php file. This file provides all the magic to autoload the Symfony components as well as our own code (see below and the PSR 4 spec). All we need to do to take advantage of this is to use the proper namespaces and in our API's entry point, index.php add require('./vendor/autoload.php'); at the top. Now we can autoload Symfony classes by useing the namespaces at the top of the file or by providing the full namespace path.

use Symfony\Component\Routing\Router;

$router = new Router();

Composer autoloading of our classes

The only thing left to do now with Composer is to be able to load our own classes once we write them. We will store our own classes in the app dir under our working directory. Classes are also autoloaded via composer. To do we map a physical directory to a namespace in composer.json. Like so:

"autoload": {
    "psr-4": {
        "App\\":"app/"
    }
}  

What is this doing? It says use the PSR 4 autoloader and map the App root namespace to the ./app directory. So lets say we have a class called Database in ./app/Database.php. Our Database class would be namespaced to App and can be used via use App\Database in any other file. The works at the subdirectory level too - more on this later.

Setting up our App object

Lets first create an App object that will be the main handler of our API. App will live in ./app/App.php and initial setup is as follows:

namespace App;

class App
{

	public function __construct()
	{
		echo 'Hi from App constructor';
	}

}

App gets instatiated via index.php:

require('./vendor/autoload.php');

use App\App;

$app = new App();

Symfony HttpFoundation component

The Symfony HttpFoundation component provides a nice set of classes for us to easily access requests and generate responses. No more messing around with $_SERVER globals and other such variables, by using HttpFoundation we can access everything in an OO fashion.

The only Request related things we really need to concern ourselves with for now is how to set up the request and get the URL path.

To set up the Request object we will modify App so it is like so:

<?php

namespace App;

use Symfony\Component\HttpFoundation\Request;

class App
{
	private $request;

	public function __construct()
	{
		$this->request = Request::createFromGlobals();
	}

	public function getRequest()
	{
		return $this->request;
	}
	
}

As you can see we keep App::$request private and use a getter to access it. We assign it on object creation, createFromGlobals initalises the Request object with data from $_SERVER etc.

Now we have our Request object initalised, we can use getPathInfo() to 'return the path being requested relative to the executed script'.

echo $this->request->getPathInfo();

Why do we need this? Well for the Router component of course!

Symfony Routing component

The routing component enables us to route an HTTP request to a specific method/class/function for our API. The routing component will be especially useful for us as you can map placeholders as you will see below.

We're going to do something a bit different to HttpFoundation component here and wrap the Symfony Router in our own ApiRouter class. There is a bit of boilerplate in setting up the Router object and there is no reason to clutter up App with that.

Here's our ./app/ApiRouter.php class in all its glory:

<?php

namespace App;

use Symfony\Component\Routing\Router;
use Symfony\Component\Routing\RequestContext;
use Symfony\Component\Routing\Loader\PhpFileLoader;
use Symfony\Component\Config\FileLocator;

class ApiRouter
{
	private $router;

	public function __construct($request)
	{
		$context = new RequestContext();
		$context->fromRequest($request);

		$routesLocator = new FileLocator(array(__DIR__));
		$this->router = new Router(
			new PhpFileLoader($routesLocator),
			'routes.php',
			array('cache_dir' => null),
			$context
		);
	}

	public function match($url)
	{
		return $this->router->match($url);
	}
}

As you can see we need to use a few different namespace imports to load in all the required classes for Router. Router itself by the way is a convenience class to more easily use the component. The Symfony Router object requires a Loader, a loader filename, an array of options and a context. The ApiRouter itself takes the Request object as a parameter so it can build the context for the Router. We will store our routes in PHP code in a separate PHP file, routes.php. We also wrap the match method so we can call it directly from ApiRouter.

Here's our routes.php file:

<?php

use Symfony\Component\Routing\RouteCollection;
use Symfony\Component\Routing\Route;

$collection = new RouteCollection();

//stats
$collection->add(
	'stats', 
	new Route('/stats', array('_controller'=>'Stats'))
);

//stats by date
$collection->add(
	'stats_date',
	new Route(
		'/stats/{date}', 
		array('_controller'=>'Stats'),
		array('date' => '[0-9]{4}-[0-9]{2}-[0-9]{2}')
	)
);

return $collection;

This file creates a new RouteCollection and adds two Routes. The first is our /stats route and the second is our /stats/{date} route. You will see the second has a wildcard and also a third parameter that specifics a regex the wildcard should conform too. This is really powerful as the router will throw an exception if the route does not match exactly. Our date in the URL must conform to NNNN-NN-NN. Our routes file then returns the RouteCollection object.

Matching and Responses

Now we have routing sorted we need to add it to our App object. Create a new private variable, $router, and in the _construct() add:

$this->router = new ApiRouter($this->request);

and create a new getter:

public function getRouter()
{
	return $this->router;
}

Now we can look to add another new method, handleRequest() to App which will do the routing:

public function handleRequest()
{
	try {
		$this->getRouter()->match( $this->getRequest()->getPathInfo() );
		echo 'The route matched!';
	} catch (\Symfony\Component\Routing\Exception\ResourceNotFoundException $e) {
		$this->getResponse()->setStatusCode(404);
		$this->getResponse()->setContent('Not found');
		$this->getResponse()->send();
	} catch (\Symfony\Component\Routing\Exception\MethodNotAllowedException $e) {
		$this->getResponse()->setStatusCode(404);
		$this->getResponse()->setContent('Method not allowed');
		$this->getResponse()->send();
	}
}

$this->router->match( $this->request->getPathInfo() ) attempts to match a route to a route in our routes.php file. If it matches we get The route matched!. If it doesn't, the Routing component throws an exception. We catch those exceptions and throw a 404 response. To do so is really easy. You can see we are using the Response object from HttpFoundation and using the setStatusCode() method on it. However we haven't set up Response yet, so let’s do that now!

Add $response private variable to App and initalise it in the constructor: $this->response = new Response();. And add the getter:

public function getResponse()
{
	return $this->response;
}

.htaccess

One more thing, now that we have handled routing we need to create an .htaccess file to route all requests (except for files and directories) though index.php. We can do that like so:

RewriteEngine on

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ /api/index.php?p=$1 [NC,L,QSA]

What's next?

Next time we will look at mapping our API requests to specific objects so they do something and then connecting to and querying MySQL.

Further reading

Composer

HttpFoundation component

Routing component