Хочу рассказать вам про интересный опыт, с которым я столкнулся на своей последней работе. Нужна была гибкая система Environment-ов. После некоторого времени экспериментов я таки добился идеального варианта. Перейдем сразу к делу.

В protected я создал environment.php c такими вот 2я классами:

environment.php
class Environment
{
	/**
	 *
	 */
	const PRODUCTION = 10;
	/**
	 *
	 */
	const STAGING = 20;
	/**
	 *
	 */
	const TESTING = 30;
	/**
	 *
	 */
	const DEVELOPMENT = 40;

	/**
	 * @var int
	 */
	protected static $current;

	/**
	 * @var
	 */
	private static $currentObj;

	/**
	 *
	 */
	public static function instance()
	{
		if(!self::$currentObj)
		{
			self::$currentObj = new self();
		}
		return self::$currentObj;
	}

	/**
	 * @param int $ENV
	 * @return bool
	 */
	public function set($ENV = Environment::DEVELOPMENT)
	{
		if(self::$current)
		{
			return false;
		}

		if(isset($_GET['DEBUG']))
		{
			$this->setEnvironment($_GET['DEBUG']);

			$_SESSION['systemEnvironment'] = $_GET['DEBUG'];

		}
		elseif(isset($_SESSION['systemEnvironment']) and $_SESSION['systemEnvironment'] !== 'off')
		{
			$this->setEnvironment($_SESSION['systemEnvironment']);

		}
		else
		{
			self::$current = $ENV;

		}
		return true;
	}


	/**
	 * @return int
	 */
	public static function getCurrent()
	{
		return self::$current;
	}


	/**
	 * @param $level
	 */
	private function setEnvironment($level)
	{
		switch($level)
		{
			case 'PRODUCTION':
				self::$current = self::PRODUCTION;
				break;
			case 'STAGING':
				self::$current = self::STAGING;
				break;
			case 'TESTING':
				self::$current = self::TESTING;
				break;
			case 'DEVELOPMENT':
				self::$current = self::DEVELOPMENT;
				break;
			case 'off':
				if(isset($_SERVER['ENVIRONMENT']))
				{
					self::$current = constant('Environment::' . strtoupper($_SERVER['ENVIRONMENT']));
				}
				$_SESSION['systemEnvironment'] = null;
				break;

		}
	}

}


/**
 * Class EnvironmentUtils
 */
class EnvironmentUtils extends Environment
{
	/**
	 * @param $rootDirectory
	 * @param $fileName
	 * @return string
	 */
	public static function getConfigFile($rootDirectory, $fileName)
	{
		$fileLink = $rootDirectory . '/config/';
		switch(parent::$current)
		{
			case Environment::DEVELOPMENT:
				$fileLink .= 'Development/';
				break;
			case Environment::PRODUCTION:
				$fileLink .= 'Production/';
				break;
			case Environment::STAGING:
				$fileLink .= 'Staging/';
				break;
			case Environment::TESTING:
				$fileLink .= 'Testing/';
				break;

		}
		return $fileLink . $fileName;
	}
}


После чего немного изменил структуру файлов в папке config:
Вот дерево файлов.
+-- Base
¦   +-- API
¦   ¦   +-- Morpher.php
¦   ¦   L-- XmlRpcClient.php
¦   +-- Common
¦   ¦   +-- Daemon.php
¦   ¦   +-- Email.php
¦   ¦   L-- Filtrator.php
¦   +-- console.php
¦   +-- DB
¦   ¦   +-- 1c.php
¦   ¦   L-- BaseConnect.php
¦   +-- main.php
¦   +-- routes.php
+-- Development
¦   +-- cli.php
¦   +-- DB
¦   ¦   +-- 1c.php
¦   ¦   +-- BaseConnect.php
¦   L-- Web.php
+-- Production
¦   +-- cli.php
¦   +-- DB
¦   ¦   +-- 1c.php
¦   ¦   +-- BaseConnect.php
¦   ¦   +-- Sape.php
¦   L-- Web.php
+-- Staging
¦   +-- cli.php
¦   +-- DB
¦   ¦   +-- 1c.php
¦   ¦   +-- BaseConnect.php
¦   L-- Web.php
L-- Testing
+-- cli.php
+-- DB
¦   +-- 1c.php
¦   L-- BaseConnect.php
L-- Web.php

В Base/Web.php находится базовый конфиг. Из серии:
return [
	'basePath'          => PROTECTED_PATH,
	'name'              => 'MyApp',
	'theme'             => 'classic',
	'language'          => 'ru',
	'defaultController' => 'user/login',
	// preloading 'log' component
	// autoloading model and component classes
	'aliases'           => [
		'bootstrap' => PROTECTED_PATH.'extensions/bootstrap',
		// change this if necessary
	],
	'preload'           => [
		'log',
		'bootstrap'
	],
	'import'            => [
		'application.models.*',
        ]

Тоже самое в cli.php. А вот в Production/Web.php чтоб не копировать постоянно какой то параметр если меняешь, я сделал так:
return CMap::mergeArray(include(PROTECTED_PATH.'config/Base/main.php'),
	[
		'components'=>
		[
			'db'         => include(dirname(__FILE__) . '/DB/BaseConnect.php'),
			'db1c'       => include(dirname(__FILE__) . '/DB/1c.php')
		]
	]
);

Таким образом я просто перезаписываю настройки бд, ну или то, что мне нужно. Теперь самое интересное. Подгрузка. index.php.

P.S.: У меня в проекте ядро yii тянется через composer:
index.php
define("PROTECTED_PATH",realpath(dirname(__FILE__)).'/protected/');
if(!file_exists(PROTECTED_PATH.'vendor/autoload.php'))
{
	die('autoload.php not found. Composer update!');
}

require_once(PROTECTED_PATH.'vendor/autoload.php');
require_once(PROTECTED_PATH.'environment.php');

if(isset($_SERVER['HTTP_ORIGIN']))
{
	if(in_array($_SERVER['HTTP_ORIGIN'],[....]))
	{
	      header('Access-Control-Allow-Origin: '.$_SERVER['HTTP_ORIGIN']);
	      header('Access-Control-Allow-Methods: GET, POST, OPTIONS, DELETE, PUT');
	      header('Access-Control-Max-Age: 1728000');
	      header('Access-Control-Allow-Credentials: true');
	      header("Access-Control-Allow-Headers: access-token, expiry, token-type, uid, client");
	      header("Access-Control-Expose-Headers: access-token, expiry, token-type, uid, client");
	}	
}


if($_SERVER['REQUEST_METHOD'] == 'OPTIONS')
{
	header("HTTP/1.0 204 No Content");
	die();
}

Environment::instance()->set(Environment::DEVELOPMENT);

switch(Environment::getCurrent())
{
	case Environment::DEVELOPMENT:

		define('YII_DEBUG', false);
		define('YII_SKIP_AUTH', true);
		define('YII_KERNEL_LOG', true);
		define('DISPLAY_ERROR_TRACE', true);
		define('YII_TRACE_LEVEL', 0);
		define('MINIFY_INTERFACE', false);

		error_reporting(E_ALL);
		ini_set('display_errors','On');
		break;
	case Environment::PRODUCTION:

		define('YII_DEBUG', false);
		define('YII_SKIP_AUTH', false);
		define('YII_KERNEL_LOG', false);
		define('DISPLAY_ERROR_TRACE', false);
		define('YII_TRACE_LEVEL', 3);
		define('MINIFY_INTERFACE', true);

		error_reporting(0);
		ini_set('display_errors','Off');
		break;
	case Environment::STAGING:

		define('YII_DEBUG', true);
		define('YII_SKIP_AUTH', false);
		define('YII_KERNEL_LOG', true);
		define('DISPLAY_ERROR_TRACE', true);
		define('YII_TRACE_LEVEL', 0);

		error_reporting(E_ALL);
		ini_set('display_errors','On');
		break;
}

/** @noinspection PhpUndefinedClassInspection */
Yii::$enableIncludePath = true; //For init Yii kernel
$config = EnvironmentUtils::getConfigFile(PROTECTED_PATH,'Web.php');

//Other constants
define('DATE_FORMAT', 'Y/m/d');
define('STATUS_NOACTIVE', 0);
define('STATUS_ACTIVE', 1);
date_default_timezone_set("Europe/Kiev");

/** @noinspection PhpUndefinedClassInspection */
Yii::createWebApplication($config)->run();



По аналогии сделано и в cli.php. Система себя отлично зарекомендовала.

Комментарии (17)


  1. affka
    17.08.2015 11:23
    +1

    А по сколько строк в каждом из небазовых (типа Web.php) конфигов? Мы обычно сводим такие настройки в один файл production.php, т.е. на каждый env — свой дополнительный файл, который мержится с base и web/cli.
    А вообще в данном аспекте «пули» быть не может, конфигурация сильно зависит от масштабов проекта. Но в одном всегда одинакого — должен быть файл config.php, который лежит в корне и которого нет в гите (на каждой машине — свой). И там идут переопределения настроек индивидуально для машины + пароли для БД/токены/… от продакшена, т.к. хранить пароли в гите — зло :)
    Недавно для своего фреймворка делал основу для конфигурация, вот такие примеры получились — github.com/jiisoft/jii-workers/tree/master/examples


  1. getId
    17.08.2015 12:36
    +1

    Что у вас за странная каша со статическими методами?

    Устанавливаете вроде как в экземпляр

    Environment::instance()->set(Environment::DEVELOPMENT);
    

    а на самом деле в статику:
    private function setEnvironment($level)
        {
            switch($level)
            {
                case 'PRODUCTION':
                    self::$current = self::PRODUCTION;
                    break;
    


    И все последующие вызовы у вас через статические методы. Зачем private static $currentObj; ?

    А по делу, есть немного другой подход через конфигурацию из окружения: github.com/vlucas/phpdotenv.
    По ссылке объясняют, что в таком подходе хорошего.


    1. alexsoft
      17.08.2015 12:59

      Поддерживаю phpdotenv.
      Очень нравится вариант конфигов в Laravel. Всё, что меняется в зависимости от окружения, выносится в .env файл. Очень удобно!


      1. SamDark
        17.08.2015 14:12

        Только не вздумайте в продакшне из файлов читать. getenv() / setenv() работают в рамках процесса, а не в рамках реквеста.


        1. alexsoft
          17.08.2015 14:49

          bool putenv ( string $setting )
          

          Adds setting to the server environment. The environment variable will only exist for the duration of the current request. At the end of the request the environment is restored to its original state.

          Dotenv именно этой функцией устанавливает переменные. И всё работает отлично.


          1. SamDark
            17.08.2015 15:04

            1. alexsoft
              17.08.2015 16:10
              +1

              У меня просто везде nginx+php-fpm.


        1. symbix
          17.08.2015 16:15

          Если даже предположить такую редкость, как тредовый SAPI — при нормально организованном деплое приложение разворачивается каждый раз в новый каталог, так что никаких побочных эффектов вызвать не должно. А на каком-нибудь шареде с mpd_php + threaded mpm (а такое бывает?) — ну там и с setlocale аналогичные проблемы.


          1. SamDark
            17.08.2015 16:31

            Вообще mpm не так уж и редок, если всё работает на апаче.


          1. SamDark
            17.08.2015 16:32

            Проблема не в каталогах, а в том, что это shared-данные, которые пишут-читают-ресетят без лока сразу неколько «клиентов».


            1. symbix
              17.08.2015 23:24

              dotenv сначала пробует $_ENV и $_SERVER, а уже потом getenv, так что проблемы быть не должно даже в таком редком случае — конечно, если пользоваться $loader-> getEnvironmentVariable(), а не напрямую getenv().


              1. SamDark
                18.08.2015 13:08

                Если задавать переменные в окружении — не должно быть. Если читать из файлов — будут обязательно.


                1. symbix
                  18.08.2015 17:03

                  А какая разница?


                  1. SamDark
                    19.08.2015 11:05

                    1. symbix
                      20.08.2015 01:19

                      А, в смысле задавать во внешнем окружении, до запуска реквеста? Так понятнее, а то непонятно — какая разница откуда читать.

                      Но все равно же, по идее, проблем не будет, если пользоваться $loader-> getEnvironmentVariable(). Там просто не дойдет до нереентерабельного getenv и прочитается из $_ENV (равно как и запишется в него же). В environment будет бардак, но а какая разница, если в $_ENV все на месте?


                      1. SamDark
                        20.08.2015 01:29

                        Если именно из $_ENV читать, проблемы нет. Разве что если на сервере более одного проекта, бардак начинается ещё и с именованием этих самых переменных. Но тут была речь про чтение именно из .env-файликов в продакшне, что, при определённых условиях, будет как раз проблемой.


      1. bohdan4ik
        17.08.2015 15:24
        +2

        Мы пользуем концепцию *local файлов. Пишем конфиг, который повторяется у всех, а потом мерджим в него main-local.php, который уникален для каждого разработчика и сервера. Не dotenv, конечно же, но явно проще, чем нагородил автор.