Yii environment. Наследования и переопределение конфигов

Хочу рассказать вам про интересный опыт, с которым я столкнулся на своей последней работе. Нужна была гибкая система 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
│   │   └── XmlRpcClient.php
│   ├── Common
│   │   ├── Daemon.php
│   │   ├── Email.php
│   │   └── Filtrator.php
│   ├── console.php
│   ├── DB
│   │   ├── 1c.php
│   │   └── BaseConnect.php
│   ├── main.php
│   ├── routes.php
├── Development
│   ├── cli.php
│   ├── DB
│   │   ├── 1c.php
│   │   ├── BaseConnect.php
│   └── Web.php
├── Production
│   ├── cli.php
│   ├── DB
│   │   ├── 1c.php
│   │   ├── BaseConnect.php
│   │   ├── Sape.php
│   └── Web.php
├── Staging
│   ├── cli.php
│   ├── DB
│   │   ├── 1c.php
│   │   ├── BaseConnect.php
│   └── Web.php
└── Testing
├── cli.php
├── DB
│   ├── 1c.php
│   └── BaseConnect.php
└── 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
    А по сколько строк в каждом из небазовых (типа Web.php) конфигов? Мы обычно сводим такие настройки в один файл production.php, т.е. на каждый env — свой дополнительный файл, который мержится с base и web/cli.
    А вообще в данном аспекте «пули» быть не может, конфигурация сильно зависит от масштабов проекта. Но в одном всегда одинакого — должен быть файл config.php, который лежит в корне и которого нет в гите (на каждой машине — свой). И там идут переопределения настроек индивидуально для машины + пароли для БД/токены/… от продакшена, т.к. хранить пароли в гите — зло :)
    Недавно для своего фреймворка делал основу для конфигурация, вот такие примеры получились — github.com/jiisoft/jii-workers/tree/master/examples
      +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.
      По ссылке объясняют, что в таком подходе хорошего.
        0
        Поддерживаю phpdotenv.
        Очень нравится вариант конфигов в Laravel. Всё, что меняется в зависимости от окружения, выносится в .env файл. Очень удобно!
          0
          Только не вздумайте в продакшне из файлов читать. getenv() / setenv() работают в рамках процесса, а не в рамках реквеста.
            0
            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 именно этой функцией устанавливает переменные. И всё работает отлично.
            0
            Если даже предположить такую редкость, как тредовый SAPI — при нормально организованном деплое приложение разворачивается каждый раз в новый каталог, так что никаких побочных эффектов вызвать не должно. А на каком-нибудь шареде с mpd_php + threaded mpm (а такое бывает?) — ну там и с setlocale аналогичные проблемы.
              0
              Вообще mpm не так уж и редок, если всё работает на апаче.
                0
                Проблема не в каталогах, а в том, что это shared-данные, которые пишут-читают-ресетят без лока сразу неколько «клиентов».
                  0
                  dotenv сначала пробует $_ENV и $_SERVER, а уже потом getenv, так что проблемы быть не должно даже в таком редком случае — конечно, если пользоваться $loader-> getEnvironmentVariable(), а не напрямую getenv().
                    0
                    Если задавать переменные в окружении — не должно быть. Если читать из файлов — будут обязательно.
                      0
                      А какая разница?
                          0
                          А, в смысле задавать во внешнем окружении, до запуска реквеста? Так понятнее, а то непонятно — какая разница откуда читать.

                          Но все равно же, по идее, проблем не будет, если пользоваться $loader-> getEnvironmentVariable(). Там просто не дойдет до нереентерабельного getenv и прочитается из $_ENV (равно как и запишется в него же). В environment будет бардак, но а какая разница, если в $_ENV все на месте?
                            0
                            Если именно из $_ENV читать, проблемы нет. Разве что если на сервере более одного проекта, бардак начинается ещё и с именованием этих самых переменных. Но тут была речь про чтение именно из .env-файликов в продакшне, что, при определённых условиях, будет как раз проблемой.
              +2
              Мы пользуем концепцию *local файлов. Пишем конфиг, который повторяется у всех, а потом мерджим в него main-local.php, который уникален для каждого разработчика и сервера. Не dotenv, конечно же, но явно проще, чем нагородил автор.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое