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
Reblogged this on drndark and commented:
internet protocol is still evolving
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.
Yes. The idea is the same.
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)