Creating an API in PHP - Part 1
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 use
ing 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 use
d 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 Route
s. 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.