Deployment tip: How to use environ variables to create different environments with PHP


If you use a framework such as Symfony2 this problem is solved for you, but if you aren’t using any framework you probably need to solve it in one way or another. Let me explain it. One typical scenario: production and development. We have one development database and another one for production. Each database has it’s own connection string. Probably we need to hard-code the connection string within the PHP code, but obviously if we are in the development environment we are going to use one connection string and the production one in the production environment. We can solve the problem with exotic tricks in the deployment script. I like to use exactly the same source code in all environments. Exactly the same means exactly the same, so I cannot change the code before pushing it to production. Mainly because normally my production environment usually it’s a cloud, and change the code it’s a mess. What can we do?

The solution that I like for this kind of problems is to use apache’s environ variables. We inject the environ variables in the virtual host configuration:

<VirtualHost *:80>
    ...
    SetEnv GONZALO_ENVIRON development
    ...
</VirtualHost>

Now we can read the environ variable easily with PHP with:

$environ = getenv('GONZALO_ENVIRON');

I’ve seen people who use this trick to avoid to hard-code our database passwords and connection string in the PHP souce code. Using this the developers cannot see the passwords (only sysadmins). I don’t like it, basically because I’m a hybrid between developer and sysadmin and this method is not agile for me, but maybe it can be useful for you.

I will show you a little example using this technique.

<?php
include(__DIR__ . '/../lib/App.php');

$environ = getenv('GONZALO_ENVIRON');
$app = new App($environ);
echo $app->run();

We have two different configuration files one for development:

<?php
return array(
    'ENVIRON' => 'DEVELOPMENT',
    'DB'      => array(
        'MAIN' => array(
            'dsn'      => 'pgsql:host=127.0.0.1;port=5432;dbname=dev_dbname',
            'username' => 'devel_username',
            'password' => 'devel_password',
            'options'  => array(
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_NAMED
            )
        )
    )
);

And another one for production

return array(
    'ENVIRON' => 'PRODUCTION',
    'DB'      => array(
        'MAIN' => array(
            'dsn'      => 'pgsql:host=xxx.xxx.xxx.xxx;port=5432;dbname=prod_dbname',
            'username' => 'prod_username',
            'password' => 'prod_password',
            'options'  => array(
                PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_NAMED
            )
        )
    )
);

Our app library:

class AppException extends Exception{}

class App
{
    private $conf;

    public function __construct($environ)
    {
        $this->conf = $this->getConfFromEnviron($environ);
    }

    private function getConfFromEnviron($environ)
    {
        $filePath = $this->getConfFilePath($environ);
        if (!is_file($filePath)) {
            throw new AppException("File '{$filePath}' not found");
        }
        return require($filePath);
    }

    private function getConfFilePath($environ)
    {
        return __DIR__ . "/../conf/{$environ}.php";
    }

    public function getFromConf($key)
    {
        $out = $this->conf;
        foreach (explode('.', $key) as $item) {
            if (isset($out[$item])) {
                $out = $out[$item];
            } else {
                throw new AppException("Key '{$key}' not found");
            }
        }
        return $out;
    }

    public function run()
    {
        $environ = $this->getFromConf('ENVIRON');
        return "Hello from {$environ}";
    }
}

But probably the best way to explain it is with the tests:

class AppTest extends PHPUnit_Framework_TestCase
{
    public function testEnvironDevelopment()
    {
        $environ = 'development';
        $app     = new App($environ);
        $this->assertEquals('Hello from DEVELOPMENT', $app->run());
    }

    public function testEnvironProduction()
    {
        $environ = 'production';
        $app     = new App($environ);
        $this->assertEquals('Hello from PRODUCTION', $app->run());
    }

    /**
     * @expectedException AppException
     */
    public function testEnvironNotFound()
    {
        $environ = 'xxxxx';
        $app     = new App($environ);
    }

    public function testGetConf()
    {
        $environ = 'development';
        $app     = new App($environ);

        $this->assertEquals('DEVELOPMENT', $app->getFromConf('ENVIRON'));
        $this->assertEquals('devel_username', $app->getFromConf('DB.MAIN.username'));
        $this->assertEquals('devel_password', $app->getFromConf('DB.MAIN.password'));
    }

    /**
     * @expectedException AppException
     */
    public function testGetConfWithKeyNotFound()
    {
        $environ = 'development';
        $app     = new App($environ);

        $app->getFromConf('DB.MAIN.xxx');
    }
}

You can see the whole code at github

About these ads

About Gonzalo Ayuso

Web Architect specialized in Open Source technologies. PHP, Python, JQuery, Dojo, PostgreSQL, CouchDB and node.js but always learning.

Posted on August 27, 2012, in php, Technology and tagged , . Bookmark the permalink. 7 Comments.

  1. Reblogged this on drndark and commented:
    internet protocol is still evolving

  2. Great post, Gonzalo. This is exactly how I typically handle this situation. In addition, you can use this same technique for PHP CLI utilities (i.e. where APACHE is not available) by setting an environment variable in the php.ini file.

  3. I use the same approach for almost all of my projects. Just instead of setting environment from the HTTP configuration I detect it based on the current URL. Here’s how I’m doing it: https://gist.github.com/1992236

    BTW Laravel uses the same approach for detecting environment – URL matching.

    • There’s something that I like and something that I don’t like within your gist. I like the idea of setting environs according to url. Looks very flexible and powerful, and we don’t need to reload the webserver with each change. I will think about it.

      In the other hand I don’t like how $_SERVER is coupled to “environs” function. I prefer to use DI to decouple the function from $_SERVER. This technique allows us to build testeable application and the use of TDD within the develop processes. But anyway the idea is the same.

      • “I prefer to use DI to decouple the function from $_SERVER.”
        What you mean by DI?

        Agree you with separating $_SERVER global from the function, would be easier to write tests. I’ll think about it.

      • Dependecy Injection. The idea as you say is decouple the function and $_SERVER superglobal (as you comment above)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 992 other followers

%d bloggers like this: