StaticConfigTrait.php 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
  5. * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  6. *
  7. * Licensed under The MIT License
  8. * For full copyright and license information, please see the LICENSE.txt
  9. * Redistributions of files must retain the above copyright notice.
  10. *
  11. * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
  12. * @link https://cakephp.org CakePHP(tm) Project
  13. * @since 3.0.0
  14. * @license https://opensource.org/licenses/mit-license.php MIT License
  15. */
  16. namespace Cake\Core;
  17. use BadMethodCallException;
  18. use InvalidArgumentException;
  19. use LogicException;
  20. /**
  21. * A trait that provides a set of static methods to manage configuration
  22. * for classes that provide an adapter facade or need to have sets of
  23. * configuration data registered and manipulated.
  24. *
  25. * Implementing objects are expected to declare a static `$_dsnClassMap` property.
  26. */
  27. trait StaticConfigTrait
  28. {
  29. /**
  30. * Configuration sets.
  31. *
  32. * @var array<string|int, array<string, mixed>>
  33. */
  34. protected static array $_config = [];
  35. /**
  36. * This method can be used to define configuration adapters for an application.
  37. *
  38. * To change an adapter's configuration at runtime, first drop the adapter and then
  39. * reconfigure it.
  40. *
  41. * Adapters will not be constructed until the first operation is done.
  42. *
  43. * ### Usage
  44. *
  45. * Assuming that the class' name is `Cache` the following scenarios
  46. * are supported:
  47. *
  48. * Setting a cache engine up.
  49. *
  50. * ```
  51. * Cache::setConfig('default', $settings);
  52. * ```
  53. *
  54. * Injecting a constructed adapter in:
  55. *
  56. * ```
  57. * Cache::setConfig('default', $instance);
  58. * ```
  59. *
  60. * Configure multiple adapters at once:
  61. *
  62. * ```
  63. * Cache::setConfig($arrayOfConfig);
  64. * ```
  65. *
  66. * @param array<string, mixed>|string $key The name of the configuration, or an array of multiple configs.
  67. * @param mixed $config The value for the config key. Generally an array of name => configuration data for adapter.
  68. * @throws \BadMethodCallException When trying to modify an existing config.
  69. * @throws \LogicException When trying to store an invalid structured config array.
  70. * @return void
  71. */
  72. public static function setConfig(array|string $key, mixed $config = null): void
  73. {
  74. if ($config === null) {
  75. if (!is_array($key)) {
  76. throw new LogicException('If config is null, key must be an array.');
  77. }
  78. foreach ($key as $name => $settings) {
  79. static::setConfig((string)$name, $settings);
  80. }
  81. return;
  82. }
  83. if (!is_string($key)) {
  84. throw new LogicException('If config is not null, key must be a string.');
  85. }
  86. if (isset(static::$_config[$key])) {
  87. throw new BadMethodCallException(sprintf('Cannot reconfigure existing key `%s`.', $key));
  88. }
  89. if (is_object($config)) {
  90. $config = ['className' => $config];
  91. }
  92. if (isset($config['url'])) {
  93. $parsed = static::parseDsn($config['url']);
  94. unset($config['url']);
  95. $config = $parsed + $config;
  96. }
  97. if (isset($config['engine']) && empty($config['className'])) {
  98. $config['className'] = $config['engine'];
  99. unset($config['engine']);
  100. }
  101. static::$_config[$key] = $config;
  102. }
  103. /**
  104. * Reads existing configuration.
  105. *
  106. * @param string $key The name of the configuration.
  107. * @return mixed|null Configuration data at the named key or null if the key does not exist.
  108. */
  109. public static function getConfig(string $key): mixed
  110. {
  111. return static::$_config[$key] ?? null;
  112. }
  113. /**
  114. * Reads existing configuration for a specific key.
  115. *
  116. * The config value for this key must exist, it can never be null.
  117. *
  118. * @param string $key The name of the configuration.
  119. * @return mixed Configuration data at the named key.
  120. * @throws \InvalidArgumentException If value does not exist.
  121. */
  122. public static function getConfigOrFail(string $key): mixed
  123. {
  124. if (!isset(static::$_config[$key])) {
  125. throw new InvalidArgumentException(sprintf('Expected configuration `%s` not found.', $key));
  126. }
  127. return static::$_config[$key];
  128. }
  129. /**
  130. * Drops a constructed adapter.
  131. *
  132. * If you wish to modify an existing configuration, you should drop it,
  133. * change configuration and then re-add it.
  134. *
  135. * If the implementing objects supports a `$_registry` object the named configuration
  136. * will also be unloaded from the registry.
  137. *
  138. * @param string $config An existing configuration you wish to remove.
  139. * @return bool Success of the removal, returns false when the config does not exist.
  140. */
  141. public static function drop(string $config): bool
  142. {
  143. if (!isset(static::$_config[$config])) {
  144. return false;
  145. }
  146. /** @phpstan-ignore-next-line */
  147. if (isset(static::$_registry)) {
  148. static::$_registry->unload($config);
  149. }
  150. unset(static::$_config[$config]);
  151. return true;
  152. }
  153. /**
  154. * Returns an array containing the named configurations
  155. *
  156. * @return array<string> Array of configurations.
  157. */
  158. public static function configured(): array
  159. {
  160. $configurations = array_keys(static::$_config);
  161. return array_map(function ($key) {
  162. return (string)$key;
  163. }, $configurations);
  164. }
  165. /**
  166. * Parses a DSN into a valid connection configuration
  167. *
  168. * This method allows setting a DSN using formatting similar to that used by PEAR::DB.
  169. * The following is an example of its usage:
  170. *
  171. * ```
  172. * $dsn = 'mysql://user:pass@localhost/database?';
  173. * $config = ConnectionManager::parseDsn($dsn);
  174. *
  175. * $dsn = 'Cake\Log\Engine\FileLog://?types=notice,info,debug&file=debug&path=LOGS';
  176. * $config = Log::parseDsn($dsn);
  177. *
  178. * $dsn = 'smtp://user:secret@localhost:25?timeout=30&client=null&tls=null';
  179. * $config = Email::parseDsn($dsn);
  180. *
  181. * $dsn = 'file:///?className=\My\Cache\Engine\FileEngine';
  182. * $config = Cache::parseDsn($dsn);
  183. *
  184. * $dsn = 'File://?prefix=myapp_cake_core_&serialize=true&duration=+2 minutes&path=/tmp/persistent/';
  185. * $config = Cache::parseDsn($dsn);
  186. * ```
  187. *
  188. * For all classes, the value of `scheme` is set as the value of both the `className`
  189. * unless they have been otherwise specified.
  190. *
  191. * Note that querystring arguments are also parsed and set as values in the returned configuration.
  192. *
  193. * @param string $dsn The DSN string to convert to a configuration array
  194. * @return array<string, mixed> The configuration array to be stored after parsing the DSN
  195. * @throws \InvalidArgumentException If not passed a string, or passed an invalid string
  196. */
  197. public static function parseDsn(string $dsn): array
  198. {
  199. if (empty($dsn)) {
  200. return [];
  201. }
  202. $pattern = <<<'REGEXP'
  203. {
  204. ^
  205. (?P<_scheme>
  206. (?P<scheme>[\w\\\\]+)://
  207. )
  208. (?P<_username>
  209. (?P<username>.*?)
  210. (?P<_password>
  211. :(?P<password>.*?)
  212. )?
  213. @
  214. )?
  215. (?P<_host>
  216. (?P<host>[^?#/:@]+)
  217. (?P<_port>
  218. :(?P<port>\d+)
  219. )?
  220. )?
  221. (?P<_path>
  222. (?P<path>/[^?#]*)
  223. )?
  224. (?P<_query>
  225. \?(?P<query>[^#]*)
  226. )?
  227. (?P<_fragment>
  228. \#(?P<fragment>.*)
  229. )?
  230. $
  231. }x
  232. REGEXP;
  233. preg_match($pattern, $dsn, $parsed);
  234. if (!$parsed) {
  235. throw new InvalidArgumentException(sprintf('The DSN string `%s` could not be parsed.', $dsn));
  236. }
  237. $exists = [];
  238. /**
  239. * @var string|int $k
  240. */
  241. foreach ($parsed as $k => $v) {
  242. if (is_int($k)) {
  243. unset($parsed[$k]);
  244. } elseif (str_starts_with($k, '_')) {
  245. $exists[substr($k, 1)] = ($v !== '');
  246. unset($parsed[$k]);
  247. } elseif ($v === '' && !$exists[$k]) {
  248. unset($parsed[$k]);
  249. }
  250. }
  251. $query = '';
  252. if (isset($parsed['query'])) {
  253. $query = $parsed['query'];
  254. unset($parsed['query']);
  255. }
  256. parse_str($query, $queryArgs);
  257. /**
  258. * @var string $key
  259. */
  260. foreach ($queryArgs as $key => $value) {
  261. if ($value === 'true') {
  262. $queryArgs[$key] = true;
  263. } elseif ($value === 'false') {
  264. $queryArgs[$key] = false;
  265. } elseif ($value === 'null') {
  266. $queryArgs[$key] = null;
  267. }
  268. }
  269. /** @var array<string, mixed> $parsed */
  270. $parsed = $queryArgs + $parsed;
  271. if (empty($parsed['className'])) {
  272. $classMap = static::getDsnClassMap();
  273. /** @var string $scheme */
  274. $scheme = $parsed['scheme'];
  275. $parsed['className'] = $scheme;
  276. if (isset($classMap[$scheme])) {
  277. $parsed['className'] = $classMap[$scheme];
  278. }
  279. }
  280. return $parsed;
  281. }
  282. /**
  283. * Updates the DSN class map for this class.
  284. *
  285. * @param array<string, string> $map Additions/edits to the class map to apply.
  286. * @return void
  287. * @psalm-param array<string, class-string> $map
  288. */
  289. public static function setDsnClassMap(array $map): void
  290. {
  291. static::$_dsnClassMap = $map + static::$_dsnClassMap;
  292. }
  293. /**
  294. * Returns the DSN class map for this class.
  295. *
  296. * @return array<string, class-string>
  297. */
  298. public static function getDsnClassMap(): array
  299. {
  300. return static::$_dsnClassMap;
  301. }
  302. }