2 namespace Drush\Config;
4 use Consolidation\Config\Loader\ConfigLoaderInterface;
5 use Drush\Config\Loader\YamlConfigLoader;
6 use Consolidation\Config\Loader\ConfigProcessor;
7 use Consolidation\Config\Util\EnvConfig;
8 use Symfony\Component\Finder\Finder;
11 * Locate Drush configuration files and load them into the configuration
14 * This class knows how to find all of the global and site-local
15 * configuration files for Drush, as long as it is provided with
16 * the necessary base directories:
18 * - The user's home directory
19 * - The values provided for --config and --alias-path
22 * There are two operating modes that are supported:
24 * - Normal: All config locations are used.
25 * - Local: The global locations are omitted.
27 * The mode is set via the `setLocal()` method.
38 protected $sources = false;
40 protected $siteRoots = [];
42 protected $composerRoot;
44 protected $configFilePaths = [];
46 protected $configFileVariant;
48 protected $processedConfigPaths = [];
53 * Specified by the script itself :
54 * process : Generated in the current process.
55 * cli : Passed as --option=value to the command line.
56 * stdin : Passed as a JSON encoded string through stdin.
57 * specific : Defined in a command-specific option record, and
58 * set in the command context whenever that command is used.
59 * alias : Defined in an alias record, and set in the
60 * alias context whenever that alias is used.
62 * Specified by config files :
63 * custom : Loaded from the config file specified by --config or -c
64 * site : Loaded from the drush.yml file in the Drupal site directory.
65 * drupal : Loaded from the drush.yml file in the Drupal root directory.
66 * user : Loaded from the drush.yml file in the user's home directory.
67 * home.drush Loaded from the drush.yml file in the $HOME/.drush directory.
68 * system : Loaded from the drush.yml file in the system's $PREFIX/etc/drush directory.
69 * drush : Loaded from the drush.yml file in the same directory as drush.php.
71 * Specified by the script, but has the lowest priority :
72 * default : The script might provide some sensible defaults during init.
75 // 'process' context is provided by ConfigOverlay
76 const ENVIRONMENT_CONTEXT = 'environment'; // This is more of a 'runtime' context
77 const PREFLIGHT_CONTEXT = 'cli';
78 // 'stdin' context not implemented
79 // 'specific' context obsolete; command-specific options handled differently by annotated command library
80 const ALIAS_CONTEXT = 'alias';
81 // custom context is obsolete (loaded in USER_CONTEXT)
82 const SITE_CONTEXT = 'site';
83 const DRUPAL_CONTEXT = 'drupal';
84 const USER_CONTEXT = 'user';
85 // home.drush is obsolete (loaded in USER_CONTEXT)
86 // system context is obsolete (loaded in USER_CONTEXT - note priority change)
87 const ENV_CONTEXT = 'env';
88 const DRUSH_CONTEXT = 'drush';
90 // 'default' context is provided by ConfigOverlay
93 * ConfigLocator constructor
95 public function __construct($envPrefix = '', $configFileVariant = '')
97 $this->configFileVariant = $configFileVariant;
98 $this->config = new DrushConfig();
100 // Add placeholders to establish priority. We add
101 // contexts from lowest to highest priority.
102 $this->config->addPlaceholder(self::DRUSH_CONTEXT);
103 if (!empty($envPrefix)) {
104 $envConfig = new EnvConfig($envPrefix);
105 $this->config->addContext(self::ENV_CONTEXT, $envConfig);
107 $this->config->addPlaceholder(self::USER_CONTEXT);
108 $this->config->addPlaceholder(self::DRUPAL_CONTEXT);
109 $this->config->addPlaceholder(self::SITE_CONTEXT); // not implemented yet (multisite)
110 $this->config->addPlaceholder(self::ALIAS_CONTEXT);
111 $this->config->addPlaceholder(self::PREFLIGHT_CONTEXT);
112 $this->config->addPlaceholder(self::ENVIRONMENT_CONTEXT);
114 $this->isLocal = false;
116 $this->configFilePaths = [];
120 * Put the config locator into 'local 'mode.
122 * @param bool $isLocal
124 public function setLocal($isLocal)
126 $this->isLocal = $isLocal;
130 * Keep track of the source that every config item originally came from.
131 * Potentially useful in debugging. If collectSources(true) is called,
132 * then the sources will be accumulated as config files are loaded. Otherwise,
133 * this information will not be saved.
135 * @param bool $collect
138 public function collectSources($collect = true)
140 $this->sources = $collect ? [] : false;
145 * Return all of the sources for every configuration item. The key
146 * is the address of the configuration item, and the value is the
147 * configuration file it was loaded from. Note that this method will
148 * return just an empty array unless collectSources(true) is called
149 * prior to loading configuration files.
153 public function sources()
155 return $this->sources;
159 * Return a list of all configuration files that were loaded.
163 public function configFilePaths()
165 return $this->configFilePaths;
169 * Accumulate the sources provided by the configuration loader.
171 protected function addToSources(array $sources)
173 if (!is_array($this->sources)) {
176 $this->sources = array_merge_recursive($this->sources, $sources);
180 * Return the configuration object. Create it and load it with
181 * all identified configuration if necessary.
185 public function config()
187 return $this->config;
191 * Exports all of the information stored in the environment, and adds
192 * it to the configuration. The Environment object itself is only
193 * available during preflight; the information exported here may be
194 * obtained by commands et. al. as needed. @see Environment::exportConfigData()
196 * @param Environment $environent
199 public function addEnvironment(Environment $environment)
201 $this->config->getContext(self::ENVIRONMENT_CONTEXT)->import($environment->exportConfigData());
206 * Add config paths defined in preflight configuration.
208 * @param array $paths
211 public function addPreflightConfigFiles($filepaths)
213 $this->addConfigPaths(self::PREFLIGHT_CONTEXT, (array) $filepaths);
218 * Take any configuration from the active alias record, and add it
219 * to our configuratino.
222 public function addAliasConfig($aliasConfig)
224 $this->config->addContext(self::ALIAS_CONTEXT, $aliasConfig);
230 * Given the path provided via --config and the user's home directory,
231 * add all of the user configuration paths.
233 * In 'local' mode, only the --config location is used.
236 public function addUserConfig($configPaths, $systemConfigPath, $userConfigDir)
238 $paths = $configPaths;
239 if (!$this->isLocal) {
240 $paths = array_merge($paths, [ $systemConfigPath, $userConfigDir ]);
242 $this->addConfigPaths(self::USER_CONTEXT, $paths);
247 * Add the Drush project directory as a configuration search location.
249 * @param $drushProjectDir path to the drush project directory
252 public function addDrushConfig($drushProjectDir)
254 if (!$this->isLocal) {
255 $this->addConfigPaths(self::DRUSH_CONTEXT, [ $drushProjectDir ]);
261 * Add any configuration files found around the Drupal root of the
264 * @param Path to the selected Drupal site
267 public function addSitewideConfig($siteRoot)
269 // There might not be a site.
270 if (!is_dir($siteRoot)) {
274 // We might have already processed this root.
275 $siteRoot = realpath($siteRoot);
276 if (in_array($siteRoot, $this->siteRoots)) {
280 // Remember that we've seen this location.
281 $this->siteRoots[] = $siteRoot;
283 $this->addConfigPaths(self::DRUPAL_CONTEXT, [ dirname($siteRoot) . '/drush', "$siteRoot/drush", "$siteRoot/sites/all/drush" ]);
288 * Add any configuration file found at any of the provided paths. Both the
289 * provided location, and the directory `config` inside each provided location
290 * is searched for a drush.yml file.
292 * @param string $contextName Which context to put all configuration files in.
293 * @param string[] $paths List of paths to search for configuration.
296 public function addConfigPaths($contextName, $paths)
298 $loader = new YamlConfigLoader();
299 // Make all of the config values parsed so far available in evaluations.
300 $reference = $this->config()->export();
301 $processor = new ConfigProcessor();
302 $context = $this->config->getContext($contextName);
303 $processor->add($context->export());
308 if ($this->configFileVariant) {
309 $candidates[] = "drush{$this->configFileVariant}.yml";
311 $candidates = $this->expandCandidates($candidates, 'config/');
312 $config_files = $this->findConfigFiles($paths, $candidates);
313 $this->addConfigFiles($processor, $loader, $config_files);
315 // Complete config import.
316 $this->addToSources($processor->sources());
317 $context->import($processor->export($reference));
318 $this->config->addContext($contextName, $context);
319 $this->processedConfigPaths = array_merge($this->processedConfigPaths, $paths);
322 if ($context->has('drush.paths.config')) {
323 $new_config_paths = array_diff((array) $context->get('drush.paths.config'), $this->processedConfigPaths);
324 if ($new_config_paths) {
325 $this->addConfigPaths($contextName, $new_config_paths);
333 * Adds $configFiles config files.
335 * @param ConfigProcessor $processor
336 * @param ConfigLoaderInterface $loader
337 * @param array $configFiles
339 protected function addConfigFiles(ConfigProcessor $processor, ConfigLoaderInterface $loader, array $configFiles)
341 foreach ($configFiles as $configFile) {
342 $processor->extend($loader->load($configFile));
343 $this->configFilePaths[] = $configFile;
348 * Given a list of paths, and candidates that might exist at each path,
349 * return all of the candidates that can be found. Candidates may be
350 * either directories or files.
352 * @param string[] $paths
353 * @param string[] $candidates
354 * @return string[] paths
356 protected function identifyCandidates($paths, $candidates)
359 foreach ($paths as $path) {
360 $configFiles = array_merge($configFiles, $this->identifyCandidatesAtPath($path, $candidates));
366 * Search for all matching candidate locations at a single path.
367 * Candidate locations may be either directories or files.
369 * @param string $path
370 * @param string[] $candidates
373 protected function identifyCandidatesAtPath($path, $candidates)
375 if (!is_dir($path)) {
380 foreach ($candidates as $candidate) {
381 $configFile = empty($candidate) ? $path : "$path/$candidate";
382 if (file_exists($configFile)) {
383 $result[] = $configFile;
390 * Get the site aliases according to preflight arguments and environment.
392 * @param $preflightArgs
393 * @param Environment $environment
397 public function getSiteAliasPaths($paths, Environment $environment)
399 // In addition to the paths passed in to us (from --alias-paths
400 // commandline options), add some site-local locations.
401 $base_dirs = array_filter(array_merge($this->siteRoots, [$this->composerRoot]));
402 $site_local_paths = array_map(
404 return "$item/drush/sites";
408 $paths = array_merge($paths, $site_local_paths);
414 * Get the commandfile paths according to preflight arguments.
416 * @param $preflightArgs
420 public function getCommandFilePaths($commandPaths, $root)
422 $builtin = $this->getBuiltinCommandFilePaths();
423 $included = $this->getIncludedCommandFilePaths($commandPaths);
424 $site = $this->getSiteCommandFilePaths(["$root/drush", dirname($root) . '/drush']);
434 * Return all of the built-in commandfile locations
436 protected function getBuiltinCommandFilePaths()
444 * Return all of the commandfile locations specified via
445 * an 'include' option.
447 protected function getIncludedCommandFilePaths($commandPaths)
450 // Commands specified by 'include' option
451 foreach ($commandPaths as $commandPath) {
452 if (is_dir($commandPath)) {
453 $searchpath[] = $commandPath;
460 * Return all of the commandfile paths in any '$root/drush' or
461 * 'dirname($root)/drush' directory that contains a composer.json
462 * file or a 'Commands' or 'src/Commands' directory.
464 protected function getSiteCommandFilePaths($directories)
468 $directories = array_filter($directories, 'is_dir');
470 if (empty($directories)) {
475 $finder = new Finder();
477 ->ignoreUnreadableDirs()
478 ->path('#composer.json$|^src/Commands|^Commands#')
482 foreach ($finder as $file) {
483 $result[] = dirname($file->getRealPath());
490 * Sets the composer root.
492 * @param $selectedComposerRoot
494 public function setComposerRoot($selectedComposerRoot)
496 $this->composerRoot = $selectedComposerRoot;
500 * Double the candidates, adding '$prefix' before each existing one.
502 public function expandCandidates($candidates, $prefix)
504 $additional = array_map(
505 function ($item) use ($prefix) {
506 return $prefix . $item;
510 return array_merge($candidates, $additional);
514 * Given an array of paths, separates files and directories.
516 * @param array $paths
517 * An array of config paths. These may be config files or paths to dirs
518 * containing config files.
519 * @param array $candidates
520 * An array filenames that are considered config files.
523 * An array. The first row is an array of files, the second row is an
526 protected function findConfigFiles($paths, $candidates)
530 foreach ($paths as $path) {
531 if (file_exists($path)) {
533 $dirs[] = realpath($path);
535 $files[] = realpath($path);
540 // Search directories for config file candidates.
541 $discovered_config_files = $this->identifyCandidates($dirs, $candidates);
543 // Merge discoverd candidates with explicitly specified config files.
544 $config_files = array_merge($discovered_config_files, $files);
546 return $config_files;