In parts one and two of this series I introduced some of the basic libraries and concepts we'll need to create our API. Now that we're into the third part I want to take a step back and look at something that can help us in the long run: a bit more structure. As I'm sure you've noticed already (check out the code on the "part2" branch of the repositoryi) things are starting to get a little crowded in our index.php
file. We've already defined the main application and changed some configuration options for custom handlers. Even just with this code it's getting a bit verbose to keep in just one file.
There's a decent amount of functionality to come in this series and, while you could keep it all in a single file, it would become a nightmare for maintenance purposes down the line. In order to help the situation, I'm going to fall back on a tried and true method for handling larger applications: the Model/View/Controller design pattern.
If you're not already familiar with this structure, here's a brief overview of each piece of the puzzle:
The goal of this structure is to split out the functionality of the application into chunks based on the Single Responsibility Principle - each class/object in the application does one thing and one thing only. There's other pieces that are included in a more robust MVC framework like service providers and other business logic handlers but we're keeping things simpler here. There'll be a little bit of extra processing involved but on the whole we'll stick with the pure MVC components.
We'll be augmenting this MVC structure a little through some middleware functionality, briefly explained in part one, to let us create reusable, single-purpose chunks of functionality that can be reused across the entire system.
There's a huge number of MVC frameworks out there in the PHP ecosystem and we could likely use any of them to accomplish most of what we're doing here. However, I want to be sure that its understood what each piece if doing and where each library fits into the picture. That way, if you decide to move to another framework in the future, you'll know what pieces to reimplement.
As you've already seen, the Slim framework is going to provide the backbone of our application, giving us the ability to route requests from a URL to the correct piece of functionality relatively easily. True to its name that's about all of the functionality it comes with. There's a few other pieces of functionality it has to offer but we're mainly wanting it for the request and response handling.
To help with some of the other functionality, there's a few other packages we're going to bring in to help us out. A lot of these kinds of features would come "for free" with larger frameworks.
vlucas/phpdotenv
This library is used to read from a .env
file in the defined directly (defaulting to the current directory). The .env
files contain settings for your application and help you keep them out of your code. They're then loaded into the $_ENV
variable for easy reference anywhere in the application.
aura/session
Slim doesn't come with a session handler by default and using PHP's own $_SESSION
functionality can get a little messy. Instead I've opted to use this package from the Aura component collection to help keep things cleaner. It works with the $_SESSION
handler internally so it's still using the same functionality, just with a friendly interface overlaid.
illuminate/database
This is the database component from the Laravel framework and makes it much simpler to work with the data in our database tables. It's an ORM (object relational mapper) tool that uses an ActiveRecord
structure to reference the entities and collections represented in your database. This package also includes the Capsule
functionality we're going to use to set up our connection.
doctrine/dbal
This library is required to have some of the manual database querying work with the Laravel database component. While it may not be required from the start it will come in handy later on if more complex queries are required.
robmorgan/phinx
Finally, we're going to install the Phinx database migration manager. The Illuminate/database
package has everything we need to work with the tables once they're created but we still need to create them. Phinx makes it easy to create migrations that can be run or rolled back as needed and is much less error prone than working with a bunch of raw SQL statements.
To get them all installed, it's just one easy command:
> composer require vlucas/phpdotenv aura/session illuminate/database doctrine/dbal robmorgan/phinx
./composer.json has been updated
Loading composer repositories with package information
Updating dependencies (including require-dev)
[...]
Writing lock file
Generating autoload files
Along with these packages comes a whole host of other dependencies, several coming from the Symfony and Doctrine side. Don't worry too much about these though. Even with them all installed along with Slim, the vendor/
directory comes in at 11MB - tiny by any application standards.
You may be asking, why do we even need these packages? Can't all of this be done with just plain PHP and SQL? Well, the answer to that is two-fold. One, these packages make development of these features faster as they're well tested and have a lot of use cases covered. The other is that these prevent a big case of Not Invented Here (NIH) syndrome that it's all too easy to fall into.
Now let's start off the process by a look at what our application will look like when we've successfully moved everything around and split it out into its functional parts.
App/
--> Controller/
--> Model/
--> View/
--> Middleware/
bootstrap/
--> app.php
--> db.php
--> routes.php
templates/
public/
db/
Lets walk through this structure piece by piece. Our main namespace will be App
for the application files. This is everything living under the App/
directory including controllers, models and any view helper classes we might need. Inside of the bootstrap
directory we'll have the main configuration files for our application. This will include some of the basic app setup (like the handlers from the previous part of the series) and the Slim application configuration. The database connection information will live in the db
config file and route setup will be in the routes
configuration file. Splitting these out makes things a bit less complex in the long run rather than a single configuration file packed with a mess of settings.
Finally we have the 'templates' directory that will house any view templates we might need, the db
directory what will house the Phinx migrations and seeds and, finally, the public
directory where we'll put our front controller index.php
file.
It's important to note that we're using a subdirectory as the document root. This helps prevent other security issues like the
.env
file with all sorts of sensitive information from being directly web accessible.
Don't worry if you don't quite get all of the pieces here and how they fit together. I'm going to walk you through each step of the way and explain what's happening at any given point.
Take some time now to go ahead in a directory to make the directories:
mkdir App
mkdir bootstrap
mkdir templates
mkdir public
mkdir db
This will get us started and in the right place to migrate what we have.
Right now what we basically have in our index.php
file with the code already defined is:
/
requestWe're going to take each of these things and break them out into our new structure. First we'll start with the bootstrapping. Let's take that code and move it out to a bootstrap/app.php
file so that it looks like this:
<?php
session_start();
require_once '../vendor/autoload.php';
$dotenv = new Dotenv\Dotenv(BASE_PATH);
$dotenv->load();
$app = new \Slim\App();
$container = $app->getContainer();
// Make the custom App autoloader
spl_autoload_register(function($class) {
$classFile = APP_PATH.'/../'.str_replace('\\', '/', $class).'.php';
if (!is_file($classFile)) {
throw new \Exception('Cannot load class: '.$class);
}
require_once $classFile;
});
// Autoload in our controllers into the container
foreach (new DirectoryIterator(APP_PATH.'/Controller') as $fileInfo) {
if($fileInfo->isDot()) continue;
$class = 'App\\Controller\\'.str_replace('.php', '', $fileInfo->getFilename());
$container[$class] = function($c) use ($class){
return new $class();
};
}
$container['notFoundHandler'] = function($container) {
return function ($request, $response) use ($container) {
return $container['response']
->withStatus(404)
->withHeader('Content-Type', 'application/json')
->write(json_encode(['error' => 'Resource not valid']));
};
};
$container['errorHandler'] = function($container) {
return function ($request, $response, $exception = null) use ($container) {
$code = 500;
$message = 'There was an error';
if ($exception !== null) {
$code = $exception->getCode();
$message = $exception->getMessage();
}
// Use this for debugging purposes
/*error_log($exception->getMessage().' in '.$exception->getFile().' - ('
.$exception->getLine().', '.get_class($exception).')');*/
return $container['response']
->withStatus($code)
->withHeader('Content-Type', 'application/json')
->write(json_encode([
'success' => false,
'error' => $message
]));
};
};
$container['notAllowedHandler'] = function($container) {
return function ($request, $response) use ($container) {
return $container['response']
->withStatus(401)
->withHeader('Content-Type', 'application/json')
->write(json_encode(['error' => 'Method not allowed']));
};
};
This is a literal copy and paste from the code we created previously. Here we're creating the application, getting the container and setting up our custom handlers for exceptions and not found/not allowed issues. There is a bit of extra code added in there at the top, however.
First off, before we even define our Slim\App
you'll notice the call to Dotenv\Dotenv
and its load
method. This looks in the base directory for a .env
file to load. I talked about the vlucas/phpdotenv
package earlier in the series and this is where we put it to use. Go ahead and, in the base folder of this project (not public/
, one level up from that), make a file named .env
and put the following in it:
DB_HOST=localhost
DB_NAME=database_name
DB_USER=database_user
DB_PASS=database_password
This will provide us with a template we'll update later to set up our database connection. These values will be loaded by the Dotenv
handling into the $_ENV
variable at runtime for use throughout the application.
If you neglect to set up this
.env
file or it's in the wrong location the package will thrown an exception and you won't be able to continue.
Next lets take a look at the custom autoloader. Since we'll want to refer to classes in the App\
namespace in various parts of our application, we need to add in a custom autoloader to handle those requests. We make use of the spl_autoload_register function to define this autoloader and, using the APP_PATH
, locate the matching file.
Just below that we're doing something Slim requires when working with controllers. As I've mentioned before Slim makes heavy use of the dependency injection container for a lot of things. This also includes resolving controllers and action methods when they're referenced from routes. In our basic route example we're just outputting something directly but that could easily be shifted over to something like this:
<?php
class IndexController
{
public function index()
{
echo 'index!';
}
}
$app->get('/', 'IndexController:index');
The GET
route defined above is a special format Slim uses to route the request correctly to the index
method in the IndexController
. However, in order to accomplish this, we need to preload the controllers. That's what the DirectoryIterator
there is doing - pulling the files out of the App\Controller
directory and loading them in to the container by name. This then makes it trivial and much more concise to define our routes (coming a bit later).
Now we're going to make our front controller in the public/index.php
file. Since we need to pull in the code from our bootstrap file, we're just going to include it at the top of the file and set up a few other constants we can use later:
<?php
define('BASE_PATH', __DIR__.'/..');
define('APP_PATH', BASE_PATH.'/App');
require_once BASE_PATH.'/vendor/autoload.php';
// Autorequire everything in BASE_PATH/bootstrap, loading app first - most important
require_once BASE_PATH.'/bootstrap/app.php';
foreach (new DirectoryIterator(BASE_PATH.'/bootstrap') as $fileInfo) {
if($fileInfo->isDot()) continue;
require_once $fileInfo->getPathname();
}
$app->run();
As you can see in the above code we're doing a few things. First we're defining two constants that can be reused across the application: the BASE_PATH
referring to the root of the web application (one level up from public/
) and the APP_PATH
that points to the App/
folder. Below that we require the Composer autoload again using the BASE_PATH
as a source.
The block of code below this loads in our previously created bootstrap/app.php
bootstrapping file first, defining the application and handlers. Then, using a DirectoryIterator, it loads in any additional files in the bootstrap/
directory. This makes it easier to add more configuration setup in the future including our database and route configs without having to remember to include them manually in your bootstrap.
The final step in the public/index.php
file example is to call the run
method on the application object. This is the method that tells Slim it should process the incoming request and output the response (the request lifecycle).
Now that we have our bootstrap code and front controller in place, we need to re-define our default /
route with the new MVC structure. Create a new file in the bootstrap/
directory: bootstrap/routes.php
. This will be autoloaded by our bootstrap/app.php
setup:
<?php
$app->get('/', '\App\Controller\IndexController:index');
As mentioned, this then points the /
request over to the IndexController
, referring to it by its namespaced location. Since we already injected these controllers into our container Slim has no problem resolving this and sending it where it needs to be. We'll be adding in this controller a bit later. For now we have one configuration file left, the database setup.
Now we're going to create the database configuration, making use of the "Capsule" functionality that comes with Laravel's Eloquent package to use the Eloquent functionality outside of a Laravel application. Since we already defined our .env
file with the database connection information, all we need here is a bit of code that sets up the capsule:
<?php
$dbconfig = [
'driver' => 'mysql',
'host' => $_ENV['DB_HOST'],
'database' => $_ENV['DB_NAME'],
'username' => $_ENV['DB_USER'],
'password' => $_ENV['DB_PASS'],
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
];
$capsule = new Illuminate\Database\Capsule\Manager;
$capsule->addConnection($dbconfig);
$capsule->setAsGlobal();
$capsule->bootEloquent();
I'm assuming the use of MySQL for this tutorial but you could use other options too. Refer to the Laravel manual to determine which ones are currently supported. In the code above we start by creating the database configuration in our $dbconfig
array using the values loaded from the .env
file. This lets us keep the credential information out of the code so it can be checked in and no sensitive information is accidentally shared.
Finally we create the $capsule
and pass in the database configuration via an addConnection
. The last two lines make it possible for us to seamlessly use the Eloquent functionality globally and do some final setup and "boot" the connection with all provided information.
We're getting into the home stretch on this part of the series. I know this has been a lot of set up, but it will make things much easier down the line and the code required will be relatively trivial because we've spent the time to bootstrap things correctly.
We're going to start with a "base" controller that will contain some simple methods we can then share across all of our controllers. Some of the OOP/MVC purists out there will probably balk at this idea but for simplicity's sake I'm just going to put them here for now. So, make a new file in App\Controller\BaseController.php
containing this:
<?php
namespace App\Controller;
class BaseController
{
protected $container;
/**
* Initialize the controller with the container
*
* @param Slim\Container $container Container instance
*/
public function __construct(\Slim\Container $container)
{
$this->container = $container;
}
/**
* Magic method to get things off of the container by referencing
* them as properties on the current object
*/
public function __get($property)
{
// Special property fetch for user
if ($property == 'user') {
return $user = $this->container->get('session')->get('user');
}
if (isset($this->container, $property)) {
return $this->container->$property;
}
return null;
}
/**
* Handle the response and put it into a standard JSON structure
*
* @param boolean $status Pass/fail status of the request
* @param string $message Message to put in the response [optional]
* @param array $addl Set of additional information to add to the response [optional]
*/
public function jsonResponse($status, $message = null, array $addl = [])
{
$output = ['success' => $status];
if ($message !== null) {
$output['message'] = $message;
}
if (!empty($addl)) {
$output = array_merge($output, $addl);
}
$response = $this->response->withHeader('Content-type', 'application/json');
$body = $response->getBody();
$body->write(json_encode($output));
return $response;
}
/**
* Handle a failure response
*
* @param string $message Message to put in response [optional]
* @param array $addl Set of additional information to add to the response [optional]
*/
public function jsonFail($message = null, array $addl = [])
{
return $this->jsonResponse(false, $message, $addl);
}
/**
* Handle a success response
*
* @param string $message Message to put in response [optional]
* @param array $addl Set of additional information to add to the response [optional]
*/
public function jsonSuccess($message = null, array $addl = [])
{
return $this->jsonResponse(true, $message, $addl);
}
}
Our BaseController
really just defines some helper methods to make the output of our JSON responses standardized. The jsonSuccess
and jsonFail
are just abstractions over the jsonResponse
method to make it more clear if we have a success or failure message to share.
It also defines another convenience method with the definition of __get
. This is a PHP magic method that will be called when a property is requested from an object that either doesn't exist or isn't public. In this case, we want to be able to get things from the container a bit easier. Plus it has a bit of extra code in there to get the user off of the session...but we'll get to that later on.
You'll also notice that we're initializing the BaseController
with a __construct
method that takes in the current container instance. Slim does this automatically when it calls the controller and this makes that accessible to the base controller and the classes that extend it.
Next we'll create that IndexController
to handle the base /
request, so in App\Controller\IndexController.php
put the following:
<?php
namespace App\Controller;
class IndexController extends \App\Controller\BaseController
{
public function index()
{
return $this->jsonSuccess('Hello world!');
}
}
You'll notice we've made use of the jsonSuccess
method here to send back our "Hello world!" message as successful and in our standard structure.
Now, with all of this in place, you should be able to test the result with a simple HTTP call to the API. First, lets start it up using the same PHP built-in web server we've used before:
cd public/
php -S localhost:8000
Now head over to this address in your browser of choice: http://localhost:8000
. If all goes well you should see this response:
{
success: true,
message: "Hello world!"
}
Or, if you prefer, you can use something like curl to make the request:
$ curl http://localhost:8000
{"success":true,"message":"Hello world!"}
So I've gone through a lot of refactoring here and added a decent amount of complexity to the API application. I know it might seem like a bit of overhead for creating such a "simple" API but trust me, when we get to adding other functionality it'll make it that much easier.
As always, you can check out the GitHub repository for the latest version of the API code we've been creating: https://github.com/psecio/secure-api. The master
branch will be the latest state while each of the "part*" branches will be code specific to that part in the series. If you're seeing errors in the code you have created locally, give the repository a look and see if there's any differences between them to try to get things back to a working state.
So, to recap, most of this third part of the series has been about refactoring to make working with the overall API simpler in the future and laying some of the groundwork for things to come. With this refactoring out of the way, we can start getting into some of the interesting stuff: working with users, logging them in and starting on on some middleware to help make the job simpler.
With over 12 years of experience in development and a focus on application security Chris is on a quest to bring his knowledge to the masses, making application security accessible to everyone. He also is an avodcate for security in the PHP community and provides application security training and consulting services.