Environment.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. <?php
  2. /**
  3. * Lithium: the most rad php framework
  4. *
  5. * @copyright Copyright 2012, Union of RAD (http://union-of-rad.org)
  6. * @license http://opensource.org/licenses/bsd-license.php The BSD License
  7. */
  8. namespace lithium\core;
  9. use lithium\util\Set;
  10. /**
  11. * The `Environment` class allows you to manage multiple configurations for your application,
  12. * depending on the context within which it is running, i.e. development, test or production.
  13. *
  14. * While those three environments are the most common, you can create any arbitrary environment
  15. * with any set of configuration, for example:
  16. *
  17. * {{{ embed:lithium\tests\cases\core\EnvironmentTest::testSetAndGetCurrentEnvironment(1-3)}}}
  18. *
  19. * You can then retrieve the configurations using the key name. The correct configuration is
  20. * returned, automatically accounting for the current environment:
  21. *
  22. * {{{ embed:lithium\tests\cases\core\EnvironmentTest::testSetAndGetCurrentEnvironment(15-15)}}}
  23. *
  24. * `Environment` also works with subclasses of `Adaptable`, allowing you to maintain separate
  25. * configurations for database servers, cache adapters, and other environment-specific classes, for
  26. * example:
  27. * {{{
  28. * Connections::add('default', array(
  29. * 'production' => array(
  30. * 'type' => 'database',
  31. * 'adapter' => 'MySql',
  32. * 'host' => 'db1.application.local',
  33. * 'login' => 'secure',
  34. * 'password' => 'secret',
  35. * 'database' => 'app-production'
  36. * ),
  37. * 'development' => array(
  38. * 'type' => 'database',
  39. * 'adapter' => 'MySql',
  40. * 'host' => 'localhost',
  41. * 'login' => 'root',
  42. * 'password' => '',
  43. * 'database' => 'app'
  44. * )
  45. * ));
  46. * }}}
  47. *
  48. * This allows the database connection named `'default'` to be connected to a local database in
  49. * development, and a production database in production. You can define environment-specific
  50. * configurations for caching, logging, even session storage, i.e.:
  51. * {{{
  52. * Cache::config(array(
  53. * 'userData' => array(
  54. * 'development' => array('adapter' => 'File'),
  55. * 'production' => array('adapter' => 'Memcache')
  56. * )
  57. * ));
  58. * }}}
  59. *
  60. * When the cache configuration is accessed in the application's code, the correct configuration is
  61. * automatically used:
  62. * {{{
  63. * $user = User::find($request->id);
  64. * Cache::write('userData', "User.{$request->id}", $user->data(), '+5 days');
  65. * }}}
  66. *
  67. * In this configuration, the above example will automatically send cache writes to the file system
  68. * during local development, and to a [ memcache](http://memcached.org/) server in production.
  69. *
  70. * When writing classes that connect to other external resources, you can automatically take
  71. * advantage of environment-specific configurations by extending `Adaptable` and implementing
  72. * your resource-handling functionality in adapter classes.
  73. *
  74. * In addition to managing your environment-specific configurations, `Environment` will also help
  75. * you by automatically detecting which environment your application is running in. For additional
  76. * information, see the documentation for `Environment::is()`.
  77. *
  78. * @see lithium\core\Adaptable
  79. */
  80. class Environment {
  81. protected static $_configurations = array(
  82. 'production' => array(),
  83. 'development' => array(),
  84. 'test' => array()
  85. );
  86. /**
  87. * Holds the name of the current environment under which the application is running. Set by
  88. * passing a `Request` object or `$_SERVER` or `$_ENV` array into `Environment::set()` (which
  89. * in turn passes this on to the _detector_ used to determine the correct environment). Can be
  90. * tested or retrieved using `Environment::is()` or `Environment::get()`.
  91. *
  92. * @see lithium\correct\Environment::set()
  93. * @see lithium\correct\Environment::is()
  94. * @see lithium\correct\Environment::get()
  95. * @var string
  96. */
  97. protected static $_current = '';
  98. /**
  99. * If `Environment::is()` is used to assign a custom closure for environment detection, a copy
  100. * is kept in `$_detector`. Otherwise, `$_detector` is `null`, and the hard-coded detector is
  101. * used.
  102. *
  103. * @see lithium\core\Environment::_detector()
  104. * @see lithium\core\Environment::is()
  105. * @var object
  106. */
  107. protected static $_detector = null;
  108. /**
  109. * Resets the `Environment` class to its default state, including unsetting the current
  110. * environment, removing any environment-specific configurations, and removing the custom
  111. * environment detector, if any has been specified.
  112. *
  113. * @return void
  114. */
  115. public static function reset() {
  116. static::$_current = '';
  117. static::$_detector = null;
  118. static::$_configurations = array(
  119. 'production' => array(),
  120. 'development' => array(),
  121. 'test' => array()
  122. );
  123. }
  124. /**
  125. * A simple boolean detector that can be used to test which environment the application is
  126. * running under. For example `Environment::is('development')` will return `true` if
  127. * `'development'` is, in fact, the current environment.
  128. *
  129. * This method also handles how the environment is detected at the beginning of the request.
  130. *
  131. * #### Custom Detection
  132. *
  133. * While the default detection rules are very simple (if the `'SERVER_ADDR'` variable is set to
  134. * `127.0.0.1`, the environment is assumed to be `'development'`, or if the string `'test'` is
  135. * found anywhere in the host name, it is assumed to be `'test'`, and in all other cases it
  136. * is assumed to be `'production'`), you can define your own detection rule set easily using a
  137. * closure that accepts an instance of the `Request` object, and returns the name of the correct
  138. * environment, as in the following example:
  139. * {{{ embed:lithium\tests\cases\core\EnvironmentTest::testCustomDetector(1-9) }}}
  140. *
  141. * In the above example, the user-specified closure takes in a `Request` object, and using the
  142. * server data which it encapsulates, returns the correct environment name as a string.
  143. *
  144. * #### Host Mapping
  145. *
  146. * The most common use case is to set the environment depending on host name. For convenience,
  147. * the `is()` method also accepts an array that matches host names to environment names, where
  148. * each key is an environment, and each value is either an array of valid host names, or a
  149. * regular expression used to match a valid host name.
  150. *
  151. * {{{ embed:lithium\tests\cases\core\EnvironmentTest::testDetectionWithArrayMap(1-5) }}}
  152. *
  153. * In this example, a regular expression is being used to match local domains
  154. * (i.e. `localhost`), as well as the built-in `.console` domain, for console requests. Note
  155. * that in the console, the environment can always be overridden by specifying the `--env`
  156. * option.
  157. *
  158. * Then, one or more host names are matched up to `'test'` and `'staging'`, respectively. Note
  159. * that no rule is present for production: this is because `'production'` is the default value
  160. * if no other environment matches.
  161. *
  162. * @param mixed $detect Either the name of an environment to check against the current, i.e.
  163. * `'development'` or `'production'`, or a closure which `Environment` will use
  164. * to determine the current environment name, or an array mapping environment names
  165. * to host names.
  166. * @return boolean If `$detect` is a string, returns `true` if the current environment matches
  167. * the value of `$detect`, or `false` if no match. If used to set a custom detector,
  168. * returns `null`.
  169. */
  170. public static function is($detect) {
  171. if (is_callable($detect)) {
  172. static::$_detector = $detect;
  173. }
  174. if (!is_array($detect)) {
  175. return (static::$_current == $detect);
  176. }
  177. static::$_detector = function($request) use ($detect) {
  178. if ($request->env || $request->command == 'test') {
  179. return ($request->env) ? $request->env : 'test';
  180. }
  181. $host = method_exists($request, 'get') ? $request->get('http:host') : '.console';
  182. foreach ($detect as $environment => $hosts) {
  183. if (is_string($hosts) && preg_match($hosts, $host)) {
  184. return $environment;
  185. }
  186. if (is_array($hosts) && in_array($host, $hosts)) {
  187. return $environment;
  188. }
  189. }
  190. return "production";
  191. };
  192. }
  193. /**
  194. * Gets the current environment name, a setting associated with the current environment, or the
  195. * entire configuration array for the current environment.
  196. *
  197. * @param string $name The name of the environment setting to retrieve, or the name of an
  198. * environment, if that environment's entire configuration is to be retrieved. If
  199. * retrieving the current environment name, `$name` should not be passed.
  200. * @return mixed If `$name` is unspecified, returns the name of the current environment name as
  201. * a string (i.e. `'production'`). If an environment name is specified, returns that
  202. * environment's entire configuration as an array.
  203. */
  204. public static function get($name = null) {
  205. $cur = static::$_current;
  206. if (!$name) {
  207. return $cur;
  208. }
  209. if ($name === true) {
  210. return isset(static::$_configurations[$cur]) ? static::$_configurations[$cur] : null;
  211. }
  212. if (isset(static::$_configurations[$name])) {
  213. return static::_processDotPath($name, static::$_configurations);
  214. }
  215. if (!isset(static::$_configurations[$cur])) {
  216. return static::_processDotPath($name, static::$_configurations);
  217. }
  218. return static::_processDotPath($name, static::$_configurations[$cur]);
  219. }
  220. protected static function _processDotPath($path, &$arrayPointer) {
  221. if (isset($arrayPointer[$path])) {
  222. return $arrayPointer[$path];
  223. }
  224. if (strpos($path, '.') === false) {
  225. return null;
  226. }
  227. $pathKeys = explode('.', $path);
  228. foreach ($pathKeys as $pathKey) {
  229. if (!is_array($arrayPointer) || !isset($arrayPointer[$pathKey])) {
  230. return false;
  231. }
  232. $arrayPointer = &$arrayPointer[$pathKey];
  233. }
  234. return $arrayPointer;
  235. }
  236. /**
  237. * Creates, modifies or switches to an existing environment configuration. To create a new
  238. * configuration, or to update an existing configuration, pass an environment name and an array
  239. * that defines its configuration:
  240. * {{{ embed:lithium\tests\cases\core\EnvironmentTest::testModifyEnvironmentConfig(1-1) }}}
  241. *
  242. * You can then add to an existing configuration by calling the `set()` method again with the
  243. * same environment name:
  244. * {{{ embed:lithium\tests\cases\core\EnvironmentTest::testModifyEnvironmentConfig(6-6) }}}
  245. *
  246. * The settings for the environment will then be the aggregate of all `set()` calls:
  247. * {{{ embed:lithium\tests\cases\core\EnvironmentTest::testModifyEnvironmentConfig(7-7) }}}
  248. *
  249. * By passing an array to `$env`, you can assign the same configuration to multiple
  250. * environments:
  251. * {{{ embed:lithium\tests\cases\core\EnvironmentTest::testSetMultipleEnvironments(5-7) }}}
  252. *
  253. * The `set()` method can also be called to manually set which environment to operate in:
  254. * {{{ embed:lithium\tests\cases\core\EnvironmentTest::testSetAndGetCurrentEnvironment(5-5) }}}
  255. *
  256. * Finally, `set()` can accept a `Request` object, to automatically detect the correct
  257. * environment.
  258. *
  259. * {{{ embed:lithium\tests\cases\core\EnvironmentTest::testEnvironmentDetection(9-10) }}}
  260. *
  261. * For more information on defining custom rules to automatically detect your application's
  262. * environment, see the documentation for `Environment::is()`.
  263. *
  264. * @see lithium\action\Request
  265. * @see lithium\core\Environment::is()
  266. * @param mixed $env The name(s) of the environment(s) you wish to create, update or switch to
  267. * (string/array), or a `Request` object or `$_SERVER` / `$_ENV` array used to
  268. * detect (and switch to) the application's current environment.
  269. * @param array $config If creating or updating a configuration, accepts an array of settings.
  270. * If the environment name specified in `$env` already exists, the values in
  271. * `$config` will be recursively merged with any pre-existing settings.
  272. * @return array If creating or updating a configuration, returns an array of the environment's
  273. * settings. If updating an existing configuration, this will be the newly-applied
  274. * configuration merged with the pre-existing values. If setting the environment
  275. * itself (i.e. `$config` is unspecified), returns `null`.
  276. */
  277. public static function set($env, $config = null) {
  278. if ($config === null) {
  279. if (is_object($env) || is_array($env)) {
  280. static::$_current = static::_detector()->__invoke($env);
  281. } elseif (isset(static::$_configurations[$env])) {
  282. static::$_current = $env;
  283. }
  284. return;
  285. }
  286. if (is_array($env)) {
  287. foreach ($env as $name) {
  288. static::set($name, $config);
  289. }
  290. return;
  291. }
  292. $env = ($env === true) ? static::$_current : $env;
  293. $base = isset(static::$_configurations[$env]) ? static::$_configurations[$env] : array();
  294. return static::$_configurations[$env] = Set::merge($base, $config);
  295. }
  296. /**
  297. * Accessor method for `Environment::$_detector`. If `$_detector` is unset, returns the default
  298. * detector built into the class. For more information on setting and using `$_detector`, see
  299. * the documentation for `Environment::is()`. The `_detector()` method is called at the
  300. * beginning of the application's life-cycle, when `Environment::set()` is passed either an
  301. * instance of a `Request` object, or the `$_SERVER` or `$_ENV` array. This object (or array)
  302. * is then passed onto `$_detector`, which returns the correct environment.
  303. *
  304. * @see lithium\core\Environment::is()
  305. * @see lithium\core\Environment::set()
  306. * @see lithium\core\Environment::$_detector
  307. * @return object Returns a callable object (anonymous function) which detects the application's
  308. * current environment.
  309. */
  310. protected static function _detector() {
  311. return static::$_detector ?: function($request) {
  312. $isLocal = in_array($request->env('SERVER_ADDR'), array('::1', '127.0.0.1'));
  313. switch (true) {
  314. case (isset($request->env)):
  315. return $request->env;
  316. case ($request->command == 'test'):
  317. return 'test';
  318. case ($request->env('PLATFORM') == 'CLI'):
  319. return 'development';
  320. case (preg_match('/^test\//', $request->url) && $isLocal):
  321. return 'test';
  322. case ($isLocal):
  323. return 'development';
  324. case (preg_match('/^test/', $request->env('HTTP_HOST'))):
  325. return 'test';
  326. default:
  327. return 'production';
  328. }
  329. };
  330. }
  331. }
  332. ?>