Pull merge.
[yaffs-website] / vendor / consolidation / site-alias / src / SiteAliasFileLoader.php
1 <?php
2 namespace Consolidation\SiteAlias;
3
4 use Consolidation\Config\Loader\ConfigProcessor;
5 use Dflydev\DotAccessData\Util as DotAccessDataUtil;
6
7 /**
8  * Discover alias files:
9  *
10  * - sitename.site.yml: contains multiple aliases, one for each of the
11  *     environments of 'sitename'.
12  */
13 class SiteAliasFileLoader
14 {
15     /**
16      * @var SiteAliasFileDiscovery
17      */
18     protected $discovery;
19
20     /**
21      * @var array
22      */
23     protected $referenceData;
24
25     /**
26      * @var array
27      */
28     protected $loader;
29
30     /**
31      * SiteAliasFileLoader constructor
32      *
33      * @param SiteAliasFileDiscovery|null $discovery
34      */
35     public function __construct($discovery = null)
36     {
37         $this->discovery = $discovery ?: new SiteAliasFileDiscovery();
38         $this->referenceData = [];
39         $this->loader = [];
40     }
41
42     /**
43      * Allow configuration data to be used in replacements in the alias file.
44      */
45     public function setReferenceData($data)
46     {
47         $this->referenceData = $data;
48     }
49
50     /**
51      * Add a search location to our discovery object.
52      *
53      * @param string $path
54      *
55      * @return $this
56      */
57     public function addSearchLocation($path)
58     {
59         $this->discovery()->addSearchLocation($path);
60         return $this;
61     }
62
63     /**
64      * Return our discovery object.
65      *
66      * @return SiteAliasFileDiscovery
67      */
68     public function discovery()
69     {
70         return $this->discovery;
71     }
72
73     /**
74      * Load the file containing the specified alias name.
75      *
76      * @param SiteAliasName $aliasName
77      *
78      * @return AliasRecord|false
79      */
80     public function load(SiteAliasName $aliasName)
81     {
82         // First attempt to load a sitename.site.yml file for the alias.
83         $aliasRecord = $this->loadSingleAliasFile($aliasName);
84         if ($aliasRecord) {
85             return $aliasRecord;
86         }
87
88         // If aliasname was provides as @site.env and we did not find it,
89         // then we are done.
90         if ($aliasName->hasSitename()) {
91             return false;
92         }
93
94         // If $aliasName was provided as `@foo` (`hasSitename()` returned `false`
95         // above), then this was interpreted as `@self.foo` when we searched
96         // above. If we could not find an alias record for `@self.foo`, then we
97         // will try to search again, this time with the assumption that `@foo`
98         // might be `@foo.<default>`, where `<default>` is the default
99         // environment for the specified site. Note that in this instance, the
100         // sitename will be found in $aliasName->env().
101         $sitename = $aliasName->env();
102         return $this->loadDefaultEnvFromSitename($sitename);
103     }
104
105     /**
106      * Given only a site name, load the default environment from it.
107      */
108     protected function loadDefaultEnvFromSitename($sitename)
109     {
110         $path = $this->discovery()->findSingleSiteAliasFile($sitename);
111         if (!$path) {
112             return false;
113         }
114         $data = $this->loadSiteDataFromPath($path);
115         if (!$data) {
116             return false;
117         }
118         $env = $this->getDefaultEnvironmentName($data);
119
120         $aliasName = new SiteAliasName($sitename, $env);
121         $processor = new ConfigProcessor();
122         return $this->fetchAliasRecordFromSiteAliasData($aliasName, $processor, $data);
123     }
124
125     /**
126      * Return a list of all site aliases loadable from any findable path.
127      *
128      * @return AliasRecord[]
129      */
130     public function loadAll()
131     {
132         $result = [];
133         $paths = $this->discovery()->findAllSingleAliasFiles();
134         foreach ($paths as $path) {
135             $aliasRecords = $this->loadSingleSiteAliasFileAtPath($path);
136             if ($aliasRecords) {
137                 foreach ($aliasRecords as $aliasRecord) {
138                     $this->storeAliasRecordInResut($result, $aliasRecord);
139                 }
140             }
141         }
142         ksort($result);
143         return $result;
144     }
145
146     /**
147      * Return a list of all available alias files. Does not include
148      * legacy files.
149      *
150      * @param string $location Only consider alias files in the specified location.
151      * @return string[]
152      */
153     public function listAll($location = '')
154     {
155         return $this->discovery()->filterByLocation($location)->findAllSingleAliasFiles();
156     }
157
158     /**
159      * Given an alias name that might represent multiple sites,
160      * return a list of all matching alias records. If nothing was found,
161      * or the name represents a single site + env, then we take
162      * no action and return `false`.
163      *
164      * @param string $sitename The site name to return all environments for.
165      * @return AliasRecord[]|false
166      */
167     public function loadMultiple($sitename, $location = null)
168     {
169         $result = [];
170         foreach ($this->discovery()->filterByLocation($location)->find($sitename) as $path) {
171             if ($siteData = $this->loadSiteDataFromPath($path)) {
172                 $location = SiteAliasName::locationFromPath($path);
173                 // Convert the raw array into a list of alias records.
174                 $result = array_merge(
175                     $result,
176                     $this->createAliasRecordsFromSiteData($sitename, $siteData, $location)
177                 );
178             }
179         }
180         return $result;
181     }
182
183     /**
184      * Given a location, return all alias files located there.
185      *
186      * @param string $location The location to filter.
187      * @return AliasRecord[]
188      */
189     public function loadLocation($location)
190     {
191         $result = [];
192         foreach ($this->listAll($location) as $path) {
193             if ($siteData = $this->loadSiteDataFromPath($path)) {
194                 $location = SiteAliasName::locationFromPath($path);
195                 $sitename = $this->siteNameFromPath($path);
196                 // Convert the raw array into a list of alias records.
197                 $result = array_merge(
198                     $result,
199                     $this->createAliasRecordsFromSiteData($sitename, $siteData, $location)
200                 );
201             }
202         }
203         return $result;
204     }
205
206     /**
207      * @param array $siteData list of sites with its respective data
208      *
209      * @param SiteAliasName $aliasName The name of the record being created
210      * @param $siteData An associative array of envrionment => site data
211      * @return AliasRecord[]
212      */
213     protected function createAliasRecordsFromSiteData($sitename, $siteData, $location = '')
214     {
215         $result = [];
216         if (!is_array($siteData) || empty($siteData)) {
217             return $result;
218         }
219         foreach ($siteData as $envName => $data) {
220             if (is_array($data) && $this->isValidEnvName($envName)) {
221                 $aliasName = new SiteAliasName($sitename, $envName, $location);
222
223                 $processor = new ConfigProcessor();
224                 $oneRecord = $this->fetchAliasRecordFromSiteAliasData($aliasName, $processor, $siteData);
225                 $this->storeAliasRecordInResut($result, $oneRecord);
226             }
227         }
228         return $result;
229     }
230
231     /**
232      * isValidEnvName determines if a given entry should be skipped or not
233      * (e.g. the "common" entry).
234      *
235      * @param string $envName The environment name to test
236      */
237     protected function isValidEnvName($envName)
238     {
239         return $envName != 'common';
240     }
241
242     /**
243      * Store an alias record in a list. If the alias record has
244      * a known name, then the key of the list will be the record's name.
245      * Otherwise, append the record to the end of the list with
246      * a numeric index.
247      *
248      * @param &AliasRecord[] $result list of alias records
249      * @param AliasRecord $aliasRecord one more alias to store in the result
250      */
251     protected function storeAliasRecordInResut(&$result, AliasRecord $aliasRecord)
252     {
253         if (!$aliasRecord) {
254             return;
255         }
256         $key = $aliasRecord->name();
257         if (empty($key)) {
258             $result[] = $aliasRecord;
259             return;
260         }
261         $result[$key] = $aliasRecord;
262     }
263
264     /**
265      * If the alias name is '@sitename', or if it is '@sitename.env', then
266      * look for a sitename.site.yml file that contains it. We also handle
267      * '@location.sitename.env' here as well.
268      *
269      * @param SiteAliasName $aliasName
270      *
271      * @return AliasRecord|false
272      */
273     protected function loadSingleAliasFile(SiteAliasName $aliasName)
274     {
275         // Check to see if the appropriate sitename.alias.yml file can be
276         // found. Return if it cannot.
277         $path = $this->discovery()
278             ->filterByLocation($aliasName->location())
279             ->findSingleSiteAliasFile($aliasName->sitename());
280         if (!$path) {
281             return false;
282         }
283         return $this->loadSingleAliasFileWithNameAtPath($aliasName, $path);
284     }
285
286     /**
287      * Given only the path to an alias file `site.alias.yml`, return all
288      * of the alias records for every environment stored in that file.
289      *
290      * @param string $path
291      * @return AliasRecord[]
292      */
293     protected function loadSingleSiteAliasFileAtPath($path)
294     {
295         $sitename = $this->siteNameFromPath($path);
296         $location = SiteAliasName::locationFromPath($path);
297         if ($siteData = $this->loadSiteDataFromPath($path)) {
298             return $this->createAliasRecordsFromSiteData($sitename, $siteData, $location);
299         }
300         return false;
301     }
302
303     /**
304      * Given the path to a single site alias file `site.alias.yml`,
305      * return the `site` part.
306      *
307      * @param string $path
308      */
309     protected function siteNameFromPath($path)
310     {
311         return $this->basenameWithoutExtension($path, '.site.yml');
312
313 // OR:
314 //        $filename = basename($path);
315 //        return preg_replace('#\..*##', '', $filename);
316     }
317
318     /**
319      * Chop off the `aliases.yml` or `alias.yml` part of a path. This works
320      * just like `basename`, except it will throw if the provided path
321      * does not end in the specified extension.
322      *
323      * @param string $path
324      * @param string $extension
325      * @return string
326      * @throws \Exception
327      */
328     protected function basenameWithoutExtension($path, $extension)
329     {
330         $result = basename($path, $extension);
331         // It is an error if $path does not end with site.yml
332         if ($result == basename($path)) {
333             throw new \Exception("$path must end with '$extension'");
334         }
335         return $result;
336     }
337
338     /**
339      * Given an alias name and a path, load the data from the path
340      * and process it as needed to generate the alias record.
341      *
342      * @param SiteAliasName $aliasName
343      * @param string $path
344      * @return AliasRecord|false
345      */
346     protected function loadSingleAliasFileWithNameAtPath(SiteAliasName $aliasName, $path)
347     {
348         $data = $this->loadSiteDataFromPath($path);
349         if (!$data) {
350             return false;
351         }
352         $processor = new ConfigProcessor();
353         return $this->fetchAliasRecordFromSiteAliasData($aliasName, $processor, $data);
354     }
355
356     /**
357      * Load the yml from the given path
358      *
359      * @param string $path
360      * @return array|bool
361      */
362     protected function loadSiteDataFromPath($path)
363     {
364         $data = $this->loadData($path);
365         if (!$data) {
366             return false;
367         }
368         $selfSiteAliases = $this->findSelfSiteAliases($data);
369         $data = array_merge($data, $selfSiteAliases);
370         return $data;
371     }
372
373     /**
374      * Given an array of site aliases, find the first one that is
375      * local (has no 'host' item) and also contains a 'self.site.yml' file.
376      * @param array $data
377      * @return array
378      */
379     protected function findSelfSiteAliases($site_aliases)
380     {
381         foreach ($site_aliases as $site => $data) {
382             if (!isset($data['host']) && isset($data['root'])) {
383                 foreach (['.', '..'] as $relative_path) {
384                     $candidate = $data['root'] . '/' . $relative_path . '/drush/sites/self.site.yml';
385                     if (file_exists($candidate)) {
386                         return $this->loadData($candidate);
387                     }
388                 }
389             }
390         }
391         return [];
392     }
393
394     /**
395      * Load the contents of the specified file.
396      *
397      * @param string $path Path to file to load
398      * @return array
399      */
400     protected function loadData($path)
401     {
402         if (empty($path) || !file_exists($path)) {
403             return [];
404         }
405         $loader = $this->getLoader(pathinfo($path, PATHINFO_EXTENSION));
406         if (!$loader) {
407             return [];
408         }
409         return $loader->load($path);
410     }
411
412     /**
413      * @return DataFileLoaderInterface
414      */
415     public function getLoader($extension)
416     {
417         if (!isset($this->loader[$extension])) {
418             return null;
419         }
420         return $this->loader[$extension];
421     }
422
423     public function addLoader($extension, DataFileLoaderInterface $loader)
424     {
425         $this->loader[$extension] = $loader;
426     }
427
428     /**
429      * Given an array containing site alias data, return an alias record
430      * containing the data for the requested record. If there is a 'common'
431      * section, then merge that in as well.
432      *
433      * @param SiteAliasName $aliasName the alias we are loading
434      * @param array $data
435      *
436      * @return AliasRecord|false
437      */
438     protected function fetchAliasRecordFromSiteAliasData(SiteAliasName $aliasName, ConfigProcessor $processor, array $data)
439     {
440         $data = $this->adjustIfSingleAlias($data);
441         $env = $this->getEnvironmentName($aliasName, $data);
442         $env_data = $this->getRequestedEnvData($data, $env);
443         if (!$env_data) {
444             return false;
445         }
446
447         // Add the 'common' section if it exists.
448         if ($this->siteEnvExists($data, 'common')) {
449             $processor->add($data['common']);
450         }
451
452         // Then add the data from the desired environment.
453         $processor->add($env_data);
454
455         // Export the combined data and create an AliasRecord object to manage it.
456         return new AliasRecord($processor->export($this->referenceData + ['env-name' => $env]), '@' . $aliasName->sitenameWithLocation(), $env);
457     }
458
459     /**
460      * getRequestedEnvData fetches the data for the specified environment
461      * from the provided site record data.
462      *
463      * @param array $data The site alias data
464      * @param string $env The name of the environment desired
465      * @return array|false
466      */
467     protected function getRequestedEnvData(array $data, $env)
468     {
469         // If the requested environment exists, we will use it.
470         if ($this->siteEnvExists($data, $env)) {
471             return $data[$env];
472         }
473
474         // If there is a wildcard environment, then return that instead.
475         if ($this->siteEnvExists($data, '*')) {
476             return $data['*'];
477         }
478
479         return false;
480     }
481
482     /**
483      * Determine whether there is a valid-looking environment '$env' in the
484      * provided site alias data.
485      *
486      * @param array $data
487      * @param string $env
488      * @return bool
489      */
490     protected function siteEnvExists(array $data, $env)
491     {
492         return (
493             is_array($data) &&
494             isset($data[$env]) &&
495             is_array($data[$env])
496         );
497     }
498
499     /**
500      * Adjust the alias data for a single-site alias. Usually, a .yml alias
501      * file will contain multiple entries, one for each of the environments
502      * of an alias. If there are no environments
503      *
504      * @param array $data
505      * @return array
506      */
507     protected function adjustIfSingleAlias($data)
508     {
509         if (!$this->detectSingleAlias($data)) {
510             return $data;
511         }
512
513         $result = [
514             'default' => $data,
515         ];
516
517         return $result;
518     }
519
520     /**
521      * A single-environment alias looks something like this:
522      *
523      *   ---
524      *   root: /path/to/drupal
525      *   uri: https://mysite.org
526      *
527      * A multiple-environment alias looks something like this:
528      *
529      *   ---
530      *   default: dev
531      *   dev:
532      *     root: /path/to/dev
533      *     uri: https://dev.mysite.org
534      *   stage:
535      *     root: /path/to/stage
536      *     uri: https://stage.mysite.org
537      *
538      * The differentiator between these two is that the multi-environment
539      * alias always has top-level elements that are associative arrays, and
540      * the single-environment alias never does.
541      *
542      * @param array $data
543      * @return bool
544      */
545     protected function detectSingleAlias($data)
546     {
547         foreach ($data as $key => $value) {
548             if (is_array($value) && DotAccessDataUtil::isAssoc($value)) {
549                 return false;
550             }
551         }
552         return true;
553     }
554
555     /**
556      * Return the name of the environment requested.
557      *
558      * @param SiteAliasName $aliasName the alias we are loading
559      * @param array $data
560      *
561      * @return string
562      */
563     protected function getEnvironmentName(SiteAliasName $aliasName, array $data)
564     {
565         // If the alias name specifically mentions the environment
566         // to use, then return it.
567         if ($aliasName->hasEnv()) {
568             return $aliasName->env();
569         }
570         return $this->getDefaultEnvironmentName($data);
571     }
572
573     /**
574      * Given a data array containing site alias environments, determine which
575      * envirionmnet should be used as the default environment.
576      *
577      * @param array $data
578      * @return string
579      */
580     protected function getDefaultEnvironmentName(array $data)
581     {
582         // If there is an entry named 'default', it will either contain the
583         // name of the environment to use by default, or it will itself be
584         // the default environment.
585         if (isset($data['default'])) {
586             return is_array($data['default']) ? 'default' : $data['default'];
587         }
588         // If there is an environment named 'dev', it will be our default.
589         if (isset($data['dev'])) {
590             return 'dev';
591         }
592         // If we don't know which environment to use, just take the first one.
593         $keys = array_keys($data);
594         return reset($keys);
595     }
596 }