Completed
Push — master ( 877b15...bcf172 )
by Greg
03:52
created

SiteAliasFileLoader::load()   B

Complexity

Conditions 3
Paths 3

Size

Total Lines 24
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 24
rs 8.9713
c 0
b 0
f 0
cc 3
eloc 8
nc 3
nop 1
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`
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
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) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
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
     * @return string[]
151
     */
152
    public function listAll()
153
    {
154
        return $this->discovery()->findAllSingleAliasFiles();
155
    }
156
157
    /**
158
     * Given an alias name that might represent multiple sites,
159
     * return a list of all matching alias records. If nothing was found,
160
     * or the name represents a single site + env, then we take
161
     * no action and return `false`.
162
     *
163
     * @param string $sitename The site name to return all environments for.
164
     * @return AliasRecord[]|false
165
     */
166
    public function loadMultiple($sitename)
167
    {
168
        if ($path = $this->discovery()->findSingleSiteAliasFile($sitename)) {
169
            if ($siteData = $this->loadSiteDataFromPath($path)) {
170
                // Convert the raw array into a list of alias records.
171
                return $this->createAliasRecordsFromSiteData($sitename, $siteData);
172
            }
173
        }
174
        return false;
175
    }
176
177
    /**
178
     * @param array $siteData list of sites with its respective data
179
     *
180
     * @param SiteAliasName $aliasName The name of the record being created
0 ignored issues
show
Bug introduced by
There is no parameter named $aliasName. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
181
     * @param $siteData An associative array of envrionment => site data
182
     * @return AliasRecord[]
183
     */
184
    protected function createAliasRecordsFromSiteData($sitename, $siteData)
185
    {
186
        $result = [];
187
        if (!is_array($siteData) || empty($siteData)) {
188
            return $result;
189
        }
190
        foreach ($siteData as $envName => $data) {
191
            if (is_array($data)) {
192
                $aliasName = new SiteAliasName($sitename, $envName);
193
194
                $processor = new ConfigProcessor();
195
                $oneRecord = $this->fetchAliasRecordFromSiteAliasData($aliasName, $processor, $siteData);
196
                $this->storeAliasRecordInResut($result, $oneRecord);
0 ignored issues
show
Security Bug introduced by
It seems like $oneRecord defined by $this->fetchAliasRecordF... $processor, $siteData) on line 195 can also be of type false; however, Consolidation\SiteAlias\...oreAliasRecordInResut() does only seem to accept object<Consolidation\SiteAlias\AliasRecord>, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
197
            }
198
        }
199
        return $result;
200
    }
201
202
    /**
203
     * Store an alias record in a list. If the alias record has
204
     * a known name, then the key of the list will be the record's name.
205
     * Otherwise, append the record to the end of the list with
206
     * a numeric index.
207
     *
208
     * @param &AliasRecord[] $result list of alias records
0 ignored issues
show
Documentation introduced by
The doc-type &AliasRecord[] could not be parsed: Unknown type name "&AliasRecord" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
209
     * @param AliasRecord $aliasRecord one more alias to store in the result
210
     */
211
    protected function storeAliasRecordInResut(&$result, AliasRecord $aliasRecord)
212
    {
213
        if (!$aliasRecord) {
214
            return;
215
        }
216
        $key = $aliasRecord->name();
217
        if (empty($key)) {
218
            $result[] = $aliasRecord;
219
            return;
220
        }
221
        $result[$key] = $aliasRecord;
222
    }
223
224
    /**
225
     * If the alias name is '@sitename', or if it is '@sitename.env', then
226
     * look for a sitename.site.yml file that contains it.
227
     *
228
     * @param SiteAliasName $aliasName
229
     *
230
     * @return AliasRecord|false
231
     */
232
    protected function loadSingleAliasFile(SiteAliasName $aliasName)
233
    {
234
        // Check to see if the appropriate sitename.alias.yml file can be
235
        // found. Return if it cannot.
236
        $path = $this->discovery()->findSingleSiteAliasFile($aliasName->sitename());
237
        if (!$path) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $path of type false|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
238
            return false;
239
        }
240
        return $this->loadSingleAliasFileWithNameAtPath($aliasName, $path);
241
    }
242
243
    /**
244
     * Given only the path to an alias file `site.alias.yml`, return all
245
     * of the alias records for every environment stored in that file.
246
     *
247
     * @param string $path
248
     * @return AliasRecord[]
249
     */
250
    protected function loadSingleSiteAliasFileAtPath($path)
251
    {
252
        $sitename = $this->siteNameFromPath($path);
253
        if ($siteData = $this->loadSiteDataFromPath($path)) {
254
            return $this->createAliasRecordsFromSiteData($sitename, $siteData);
255
        }
256
        return false;
257
    }
258
259
    /**
260
     * Given the path to a single site alias file `site.alias.yml`,
261
     * return the `site` part.
262
     *
263
     * @param string $path
264
     */
265
    protected function siteNameFromPath($path)
266
    {
267
        return $this->basenameWithoutExtension($path, '.site.yml');
268
    }
269
270
    /**
271
     * Chop off the `aliases.yml` or `alias.yml` part of a path. This works
272
     * just like `basename`, except it will throw if the provided path
273
     * does not end in the specified extension.
274
     *
275
     * @param string $path
276
     * @param string $extension
277
     * @return string
278
     * @throws \Exception
279
     */
280
    protected function basenameWithoutExtension($path, $extension)
281
    {
282
        $result = basename($path, $extension);
283
        // It is an error if $path does not end with site.yml
284
        if ($result == basename($path)) {
285
            throw new \Exception("$path must end with '$extension'");
286
        }
287
        return $result;
288
    }
289
290
    /**
291
     * Given an alias name and a path, load the data from the path
292
     * and process it as needed to generate the alias record.
293
     *
294
     * @param SiteAliasName $aliasName
295
     * @param string $path
296
     * @return AliasRecord|false
297
     */
298
    protected function loadSingleAliasFileWithNameAtPath(SiteAliasName $aliasName, $path)
299
    {
300
        $data = $this->loadSiteDataFromPath($path);
301
        if (!$data) {
302
            return false;
303
        }
304
        $processor = new ConfigProcessor();
305
        return $this->fetchAliasRecordFromSiteAliasData($aliasName, $processor, $data);
306
    }
307
308
    /**
309
     * Load the yml from the given path
310
     *
311
     * @param string $path
312
     * @return array|bool
313
     */
314
    protected function loadSiteDataFromPath($path)
315
    {
316
        $data = $this->loadData($path);
317
        if (!$data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
318
            return false;
319
        }
320
        $selfSiteAliases = $this->findSelfSiteAliases($data);
321
        $data = array_merge($data, $selfSiteAliases);
322
        return $data;
323
    }
324
325
    /**
326
     * Given an array of site aliases, find the first one that is
327
     * local (has no 'host' item) and also contains a 'self.site.yml' file.
328
     * @param array $data
0 ignored issues
show
Bug introduced by
There is no parameter named $data. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
329
     * @return array
330
     */
331
    protected function findSelfSiteAliases($site_aliases)
332
    {
333
        foreach ($site_aliases as $site => $data) {
334
            if (!isset($data['host']) && isset($data['root'])) {
335
                foreach (['.', '..'] as $relative_path) {
336
                    $candidate = $data['root'] . '/' . $relative_path . '/drush/sites/self.site.yml';
337
                    if (file_exists($candidate)) {
338
                        return $this->loadData($candidate);
339
                    }
340
                }
341
            }
342
        }
343
        return [];
344
    }
345
346
    /**
347
     * Load the contents of the specified file.
348
     *
349
     * @param string $path Path to file to load
350
     * @return array
351
     */
352
    protected function loadData($path)
353
    {
354
        if (empty($path) || !file_exists($path)) {
355
            return [];
356
        }
357
        $loader = $this->getLoader(pathinfo($path, PATHINFO_EXTENSION));
358
        if (!$loader) {
359
            return [];
360
        }
361
        return $loader->load($path);
362
    }
363
364
    /**
365
     * @return DataFileLoaderInterface
366
     */
367
    public function getLoader($extension)
368
    {
369
        if (!isset($this->loader[$extension])) {
370
            return null;
371
        }
372
        return $this->loader[$extension];
373
    }
374
375
    public function addLoader($extension, DataFileLoaderInterface $loader)
376
    {
377
        $this->loader[$extension] = $loader;
378
    }
379
380
    /**
381
     * Given an array containing site alias data, return an alias record
382
     * containing the data for the requested record. If there is a 'common'
383
     * section, then merge that in as well.
384
     *
385
     * @param SiteAliasName $aliasName the alias we are loading
386
     * @param array $data
387
     *
388
     * @return AliasRecord|false
389
     */
390
    protected function fetchAliasRecordFromSiteAliasData(SiteAliasName $aliasName, ConfigProcessor $processor, array $data)
391
    {
392
        $data = $this->adjustIfSingleAlias($data);
393
        $env = $this->getEnvironmentName($aliasName, $data);
394
        if (!$this->siteEnvExists($data, $env)) {
395
            return false;
396
        }
397
398
        // Add the 'common' section if it exists.
399
        if (isset($data['common']) && is_array($data['common'])) {
400
            $processor->add($data['common']);
401
        }
402
403
        // Then add the data from the desired environment.
404
        $processor->add($data[$env]);
405
406
        // Export the combined data and create an AliasRecord object to manage it.
407
        return new AliasRecord($processor->export($this->referenceData), '@' . $aliasName->sitename(), $env);
408
    }
409
410
    /**
411
     * Determine whether there is a valid-looking environment '$env' in the
412
     * provided site alias data.
413
     *
414
     * @param array $data
415
     * @param string $env
416
     * @return bool
417
     */
418
    protected function siteEnvExists(array $data, $env)
419
    {
420
        return (
421
            is_array($data) &&
422
            isset($data[$env]) &&
423
            is_array($data[$env])
424
        );
425
    }
426
427
    /**
428
     * Adjust the alias data for a single-site alias. Usually, a .yml alias
429
     * file will contain multiple entries, one for each of the environments
430
     * of an alias. If there are no environments
431
     *
432
     * @param array $data
433
     * @return array
434
     */
435
    protected function adjustIfSingleAlias($data)
436
    {
437
        if (!$this->detectSingleAlias($data)) {
438
            return $data;
439
        }
440
441
        $result = [
442
            'default' => $data,
443
        ];
444
445
        return $result;
446
    }
447
448
    /**
449
     * A single-environment alias looks something like this:
450
     *
451
     *   ---
452
     *   root: /path/to/drupal
453
     *   uri: https://mysite.org
454
     *
455
     * A multiple-environment alias looks something like this:
456
     *
457
     *   ---
458
     *   default: dev
459
     *   dev:
460
     *     root: /path/to/dev
461
     *     uri: https://dev.mysite.org
462
     *   stage:
463
     *     root: /path/to/stage
464
     *     uri: https://stage.mysite.org
465
     *
466
     * The differentiator between these two is that the multi-environment
467
     * alias always has top-level elements that are associative arrays, and
468
     * the single-environment alias never does.
469
     *
470
     * @param array $data
471
     * @return bool
472
     */
473
    protected function detectSingleAlias($data)
474
    {
475
        foreach ($data as $key => $value) {
476
            if (is_array($value) && DotAccessDataUtil::isAssoc($value)) {
477
                return false;
478
            }
479
        }
480
        return true;
481
    }
482
483
    /**
484
     * Return the name of the environment requested.
485
     *
486
     * @param SiteAliasName $aliasName the alias we are loading
487
     * @param array $data
488
     *
489
     * @return string
490
     */
491
    protected function getEnvironmentName(SiteAliasName $aliasName, array $data)
492
    {
493
        // If the alias name specifically mentions the environment
494
        // to use, then return it.
495
        if ($aliasName->hasEnv()) {
496
            return $aliasName->env();
497
        }
498
        return $this->getDefaultEnvironmentName($data);
499
    }
500
501
    /**
502
     * Given a data array containing site alias environments, determine which
503
     * envirionmnet should be used as the default environment.
504
     *
505
     * @param array $data
506
     * @return string
507
     */
508
    protected function getDefaultEnvironmentName(array $data)
509
    {
510
        // If there is an entry named 'default', it will either contain the
511
        // name of the environment to use by default, or it will itself be
512
        // the default environment.
513
        if (isset($data['default'])) {
514
            return is_array($data['default']) ? 'default' : $data['default'];
515
        }
516
        // If there is an environment named 'dev', it will be our default.
517
        if (isset($data['dev'])) {
518
            return 'dev';
519
        }
520
        // If we don't know which environment to use, just take the first one.
521
        $keys = array_keys($data);
522
        return reset($keys);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression reset($keys); of type string|false adds false to the return on line 522 which is incompatible with the return type documented by Consolidation\SiteAlias\...tDefaultEnvironmentName of type string. It seems like you forgot to handle an error condition.
Loading history...
523
    }
524
}
525