Multiple Environments

Are there any plans to add “environment support” to Yii 2.0? Right now one of the first things I do for every project is to add different config files that are merged according to the environment used (development,testing, staging, production etc.) which can easily become messy so I was wondering if this shouldn’t be part of the Yii core to make this a bit easier.

I found Laravel’s environment support quite interesting. You actually specify an environment in a separate environment config like this


$environments = array(


    'local' => array('http://localhost*', '*.dev'),


);

Meaning everything beginning with "http://localhost*" or ending with ".dev" will use the "local" environment => local config. Any thoughts on this?

Yes, there are plans. Ideas are very welcome.

Environments is not only a great idea to deal with development/staging/production stages. Is also a great idea to deal, for example, with front/back-end. I already have a project that simulates the environment feature on 1.1.

The file structure:

protected/

config/

main.php


environment/


  frontend.php


  backend.php


  api.php


  support.php

modules/

frontend/


backend/


api/


support/

The workflow:

. Loads a main config file.

. Checks the address to see which environment are being requested.

. Loads the corresponding environment module and configuration file, making that module the root one for the request.

With this, I have the following examples:

http://frontend.application.com/ => the frontend default controller

http://backend.application.com/auth/login => backend/auth/login action

http://support.application.com/clients/tickets/create => support/clients/tickets/create action

Now I’m figuring out the best way to improve this to easily manage with development/staging/production states.

Suggestions are welcome.

In my case I have to deal not only with the environments like local/development/production, but also with the fact that I have a multiple-domain application, so I have a configuration file for each domain.

I’m loading my configuration files based on the domain name - that way I can have any number of environments I like and multi-site application at the same time.

Basically what I do is:

local.example.com usually is my local machine I work on, so the configuration file is named "local.example.com.php"

dev.example.com usually is the testing/development server, so the configuration file is "dev.example.com.php"

example.com is the production server, so the config file is named "example.com" and the "www." part is str_replaced from the domain name so example.com and www.example.com can be used.

Of course I have a global configuration file named "main.php" where most of the configuration options that are identical to all websites and environments reside. The site (environments) options overwrite the global options if necessarily.

My /config folder looks like this:




/config

   /sites

      local.example.com.php

      dev.example.com.php

      example.com.php

      ...

   main.php



If we are working more than one person on the project, we usually prefix our local copies not with "local" but with our nicknames or some other prefix, so it looks like this:




psih.example.com.php

somedude.example.com.php

sodeotherdude.example.com.php



The good thing about that is we can change the configuration files and see the SVN history on those and see who is working on the project too.

My index file is pretty straight forward




// Config base dir

$base = dirname(__FILE__).DIRECTORY_SEPARATOR.'protected'.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR;


// Get the domain

$domain = $_SERVER['SERVER_NAME'];

if (strpos($domain, 'www.') !== false) {

	$domain = str_replace('www.', '', $domain);

}


// You could show here something else, we just use the dev server configuration

// Or even remove this check in production at all - it will save stat operations

if (!file_exists($base.'sites'.DIRECTORY_SEPARATOR.$domain.'.php')) {

	$domain = 'dev.example.com';

}


$main = include($base.'main.php');

$domain_config = include($base.DIRECTORY_SEPARATOR.'sites'.DIRECTORY_SEPARATOR.$domain.'.php');


// Merge configs

$config = CMap::mergeArray($main, $domain_config);


// Create application instance

$app = Yii::createWebApplication($config);



What I don’t like in all those examples of making environments in Yii on the internet is that they all rely on some file that is not in source version control system that has to be created and read to identify witch server it is and it takes some syscalls (when you are DDOS’ed with ~10-15k of bots - not a good idea). In my case I don’t have to rely on any external file that is not in my source version control system. As a benefit I just made identical domains on my work and home machine for projects and same credentials for MySQL - so I have only one config file and if I change something in it - it is fetched from the source version control system. And because I work rarely at home - I don’t have to remember what I have changed and apply that to my home copy by hand and vice-verse.

Because I can have multiple domains to the same environment, I don’t do that job on index.php, but on what I called AddressManager that is an preloaded CApplicationComponent that checks the domain on a database table. Something like this:




class AddressManager extends CApplicationComponent

{

	const FRONTEND = 1;

	const BACKEND = 2;

	const API = 3;

	const SUPPORT = 4;


	public function init() {

		if (!$address = Yii::app()->cache->get('host.' . $_SERVER['HTTP_HOST'])) {

			$address = Yii::app()->db->createCommand()

				->select('environment')

				->from('addresses')

				->where('address RLIKE :host', array(':host' => $_SERVER['HTTP_HOST']))

				->queryRow();


			Yii::app()->cache->set('host.' . $_SERVER['HTTP_HOST'], $address);

		}


		switch ($address['environment']) {

			case AddressManager::FRONTEND:

				Yii::app()->configure(FrontendModule::config());

				break;

			case AddressManager::BACKEND:

				Yii::app()->configure(BackendModule::config());

				break;

			case AddressManager::API:

				Yii::app()->configure(ApiModule::config());

				break;

			case AddressManager::SUPPORT:

				Yii::app()->configure(SupportModule::config());

                                break;

		}

	}

}



Simple but so far so good.

My idea:

Have the following folder structure:

In main config, create an ‘env’ property that accepts environments as key/value pairs:

I would suggest at least 4 methods of determining the current environment:

  • uri: if one of the supplied strings matches the current domain: $_SERVER[‘SERVER_NAME’]

  • docRoot: if one of the supplied strings (partially) matches the current docroot: $_SERVER[‘DOCUMENT_ROOT’]

  • envVar: if one of the supplied variables exists as environment var, in this case getenv(‘myvar’)==‘prod’

  • serverIp: if one of the supplied strings matches the current server IP: $_SERVER[‘SERVER_ADDR’]

On loading page, the environments are looped, if the first doesn’t match, go to the next one, etc. You could also provide a key without a value that is automatically matched. For example, have an ‘development’ at the end of the list, as a fallback if other conditions are not found.

If an environment matches, include it and merge it with the current config. Store the environment in constant YII_ENV.

I really like the idea of wisp, especially that you could configure all default settings in main.php and only overwrite the environment specific options.

Little addition: I would like it if an environment-config would not HAVE to be in the “env”-folder but you should also be able to set a path to the file. That way I don’t need all my environment-configs in source control and I can overwrite the entire application with a new version.

Nice approach. To take this even further:

What about using folders named like the corresponding environment? This way all the config arrays of a single environment could be automatically merged. I personally like dividing configs into smaller ones (like extension configurations etc.). The environments.php file in the main folder would just hold all the environment stuff and main.php would be used as the default config if no environment is used.




config

--production

----main.php

----params.php

----extensions.php

--testing

----main.php

----params.php

----extensions.php

--staging

----main.php

----params.php

----extensions.php

main.php

environments.php



So if using the production environment all files within the production folder would get merged

What about the following setup? You could expand each environment with some additional stuff like custom imports and basic access rules




'env' => array(

    'dev' => array(

        'match' => array(

            'uri' => array('example.com', 'test.com', 'admin.*'),

            'docRoot' => '/www/mysite'

        ),

        'import' => 'application.config.env.dev',

        'debug' => true,

    ),


    'staging' => array(

        'match' => array(

            'docRoot' => '/www/mysite'

        ),

        'import' => 'application.config.staging',

        'whitelist' => array(

            'ip' => array('127.0.0.1', '31.22.155.12'),

            'message' => 'This is a testing server, ask info@example.com for access.'

        ),

       'debug' => true,

    ),


    'production' => array(

        'match' => array(

            'docRoot' => '/www/mysite',

            'envVar' => array('myvar' => 'prod')

        ),

        'import' => array(

            'application.config.staging',

            'application.config.extensions.*',

        ),

        'blacklist' => array(

            'ip' => array('127.0.0.1', '31.22.155.12'),

            'redirect' => '/site/notallowed/'

        ),

    )

),



Please tell me what you guys think…

In my apps I do the following:

  • Add an environment variable in the Web server’s virtual host configuration. In nginx: fastcgi_param APP_ENV “development”;

  • In the bootstrap file index.php (and in the file yiic.php for the console app), I check for the environment variable and the set the debug mode if needed and merge the common and the environment-specific configuration files.

Additional thoughts:

Besides the environment name, I believe that it could be useful to have an environment behavior (not Yii behaviors).

With these behaviors, multiple environments could have the same configuration without the need for multiple files with the exact same contents.

A use case is for the "production" and "demo" environments, which could have mostly the same configuration.

I found it easy merging everything but the URL rules. I still have to work on it.

For the local configuration, I use a main-local.php.template file and each developer has a main-local.php file, which is based off the template and is ignored by the SCM.

Environment are tightly related to development & deployment workflow (product cycle).

For example - small projects are suitable to deploy directly from localhost to online.

Bigger projects need more staged workflow of deployment, e.g. localhost -> testing -> production.

On most projects, we use following environments:




phase 1: -> local development (devel environment)

phase 2: -> test server 1 (local) (testing environment)

phase 3: -> test server 2 (production server copy) (testing environment)

phase 4: -> production server (production environment)




Since Yii 1.x does not assume multiple environments, we solved the issue via loading config.php files in a cascading style - meaning following configuration file can overwrite any previously loaded configuration setting. This actually the same logic like in other frameworks, e.g. ZF1.

Here is example:

(1) in /config/main.php:




...

// some production config code (always loaded first)

...

if(!Config::isProduction() && is_file(dirname(__FILE__).'/main-test.php')){

	require(dirname(__FILE__).'/main-test.php');

}



(2) in /config/main-test.php:




...

// some testing config code (overrides some configuration from main.php)

...

if(Config::isDevelopment() && is_file(dirname(__FILE__).'/main-development.php')){

	require(dirname(__FILE__).'/main-development.php');

}



(3) in /config/main-development.php:




...

// some development config code shared amongst all developers 

// (overrides configuration from main.php and main-test.php)

...

if(is_file(dirname(__FILE__).'/main-developer.php')){

	require(dirname(__FILE__).'/main-developer.php');

}



(4) in /config/main-developer.php:




...

// setting specific to particular developer, this file is never committed 

// (overrides configuration from main.php, main-test.php and main-development.php)

...



The Config object is simply check for specific IPs e.g.:





class Config{


   public static function isProduction(){

      // return TRUE if current server is running on domain "*.mydomain.com"

      return false !== stripos($_SERVER['SERVER_NAME'], 'mydomain.com');

   }


   public static function isDevelopment(){

      // return TRUE if current server is in intranet with IP 10.20.30.*

      return false !== strpos($_SERVER['SERVER_NAME'], '10.20.30.');

   }


}




Perhaps this gives some ideas on solving the issue with multiple environments.

Unfortunatelly, it is impossible to unify deployment workflows - so in the end it is questionable whether framework can solve such a specific need…

Cheers.

Lubos

This is something that I miss

Something like Zend does works for me, using enviroments as sections

We would have to adapt to accept an array instead of an ini file, something like:





return array(

 'production'=>array(

  //..main config

),

 'dev:production'=>array(

  //merges with production if enviroment is dev

 ),

 'console'=>array(

 //console config

 ),

 'dev:console'=>array(

  //dev console config

 ),

);



and init it like:




Yii::createWebApplication('config', 'application enviroment');



or




Yii::createConsoleApplication('config', 'console enviroment');



I use a combination of some of the above.

I have a separate bootstrap.php which, like the config file, returns an array like this one:




return array(

    'debug' => true,

    'traceLevel' => 3,

    'frameworkPath' => '/path/to/Yii/framework',

    'configFile' => '/path/to/config/main.php',

);



In my index.php I include this bootstrap array and use its values to start the application. For smaller projects I just change the values by hand when I need a different environment, but obviously any value (like ‘debug’) can also be set based on a certain condition (like the ip address or domain name).

Next to this I have a separate config file for each environment.