Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Dependency Injection With OpenWhisk PHP

DZone's Guide to

Dependency Injection With OpenWhisk PHP

Using a Dependency Injection container is one way to ensure that your serverless app can be easily maintained and that the app's functions are separated appropriately.

· Web Dev Zone ·
Free Resource

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Any non-trivial PHP applications use various components to do their work, from PDO through to classes from Packagist. It's fairly common in a standard PHP application to use Dependency Injection to configure and load these classes when necessary. How do we do this in a serverless environment such as OpenWhisk?

This question comes up because we do not have a single entry point into our application, instead, we have one entry point per action. If we're using Serverless to write an API, then we probably have a set of actions for reading, creating, updating and deleting resources all within the same project.

Consider a project that uses PDO to communicate with the database. The PDO object will need to be instantiated with a DSN string containing the host name, database, credentials, etc. Its likely that these will be stored in the parameters array that's passed to the action and set up either as package parameters or service bindings if you're using IBM Cloud Functions.

For this example, I have an ElephantSQL database set up in IBM's OpenWhisk service and I used Lorna Mitchell's rather helpful Bind Services to OpenWhisk Packages article to make the credentials available to my OpenWhisk actions.

Using a PDO Instance Within an Action

Consider this action which returns a list of to-do items from the database. Firstly, we instantiate and configure the PDO instance and then create a mapper object that can fetch the todo items:

function main(array $args) : array
{
    if (!isset($args['__bx_creds']['elephantsql']['uri'])) {
        throw new Exception("ElephantSQL instance has not been bound");
    }
    $credentials = parse_url($args['__bx_creds']['elephantsql']['uri']);

    $host = $credentials['host'];
    $port = $credentials['port'];
    $dbName = trim($credentials['path'], '/');
    $user = $credentials['user'];
    $password = $credentials['pass'];

    $dsn = "pgsql:host=$host;port=$port;dbname=$dbName;user=$user;password=$password";

    $pdo = new PDO($dsn);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // now we can use $pdo to interact with our PostgreSQL database via a mapper
    $mapper = new TodoMapper($pdo);
    $todos = $mapper->fetchAll();

    return [
        'statusCode' => 200,
        'body' => $todos, 
    ]; 
}

That's quite a lot of set up code that clearly doesn't belong here, especially as we need to do the same thing in every action in the project that connects to the database. We are also going to probably put our database access code in a mapper class that takes the PDO instance as a dependency, so, to my mind, it makes sense to use a DI container in our project.

I chose to use the Pimple DI container because it's nice, simple and fast.

To use it, I extended Pimple\Container and added my factory to the constructor:

<?php 
namespace App;

use InvalidArgumentException;
use PDO;
use Pimple\Container;

class AppContainer extends Container
{
    public function __construct(array $args)
    {
        if (!isset($args['__bx_creds']['elephantsql']['uri'])) {
            throw new InvalidArgumentException("ElephantSQL instance has not been bound");
        }
        $credentials = parse_url($args['__bx_creds']['elephantsql']['uri']);

        /**
         * Factory to create a PDO instance
         */
        $configuration[PDO::class] = function (Container $c) use ($credentials) {
            $host = $credentials['host'];
            $port = $credentials['port'];
            $dbName = trim($credentials['path'], '/');
            $user = $credentials['user'];
            $password = $credentials['pass'];

            $dsn = "pgsql:host=$host;port=$port;dbname=$dbName;user=$user;password=$password";

            $pdo = new PDO($dsn);
            $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
            return $pdo;
        };

        /**
         * Factory to create a TodoMapper instance
         */
        $configuration[TodoMapper::class] = function (Container $c) : TodoMapper {
            return new TodoMapper($c[PDO::class]);
        };

        parent::__construct($configuration);
    }

}

In this code, we create two factories: one to create the PDO instance and one to create a mapper class called TodoMapper. TheTodoMapper class composes a PDO instance via its constructor, so in the factory for theTodoMapper, we retrieve the PDO instance from the container.

The nice thing about doing it this way is that if an action doesn't use aTodoMapper, then the connection to the database doesn't get made as it's not needed.

Using the DIC in an Action

The code in our action gets much cleaner and easier to read now:

function main(array $args) : array
{
    $container = new App\AppContainer($args);

    $mapper = $container[App\TodoMapper::class];
    $todos = $mapper->fetchAll();

    return [
        'statusCode' => 200,
        'body' => $todos, 
    ]; 
}

In fact, it now looks much more like a controller action that you'd see in a standard PHP framework-based app.

Summary

Even in serverless applications, a clean software design is required. All the code does not belong in the action's entry function and should be separated appropriately. Using a Dependency Injection container is one way to handle this and helps ensure that your serverless application can be easily maintained.

Deploy code to production now. Release to users when ready. Learn how to separate code deployment from user-facing feature releases with LaunchDarkly.

Topics:
web dev ,openwhisk ,php development ,dependency injection

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}