Completed
Pull Request — master (#1)
by Greg
06:16
created

SiteAliasFileLoader::loadLocation()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
cc 3
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
     * @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)
168
    {
169
        if ($path = $this->discovery()->findSingleSiteAliasFile($sitename)) {
170
            if ($siteData = $this->loadSiteDataFromPath($path)) {
171
                // Convert the raw array into a list of alias records.
172
                return $this->createAliasRecordsFromSiteData($sitename, $siteData);
173
            }
174
        }
175
        return false;
176
    }
177
178
179
    /**
180
     * Given a location, return all alias files located there.
181
     *
182
     * @param string $location The location to filter.
183
     * @return AliasRecord[]
184
     */
185
    public function loadLocation($location)
186
    {
187
        $result = [];
188
        foreach ($this->listAll($location) as $path) {
189
            if ($siteData = $this->loadSiteDataFromPath($path)) {
190
                $sitename = $this->siteNameFromPath($path);
191
                // Convert the raw array into a list of alias records.
192
                $result = array_merge(
193
                    $result,
194
                    $this->createAliasRecordsFromSiteData($sitename, $siteData, $location)
195
                );
196
            }
197
        }
198
        return $result;
199
    }
200
201
    /**
202
     * @param array $siteData list of sites with its respective data
203
     *
204
     * @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...
205
     * @param $siteData An associative array of envrionment => site data
206
     * @return AliasRecord[]
207
     */
208
    protected function createAliasRecordsFromSiteData($sitename, $siteData, $location = '')
209
    {
210
        $result = [];
211
        if (!is_array($siteData) || empty($siteData)) {
212
            return $result;
213
        }
214
        foreach ($siteData as $envName => $data) {
215
            if (is_array($data)) {
216
                $aliasName = new SiteAliasName($sitename, $envName, $location);
217
218
                $processor = new ConfigProcessor();
219
                $oneRecord = $this->fetchAliasRecordFromSiteAliasData($aliasName, $processor, $siteData);
220
                $this->storeAliasRecordInResut($result, $oneRecord);
0 ignored issues
show
Security Bug introduced by
It seems like $oneRecord defined by $this->fetchAliasRecordF... $processor, $siteData) on line 219 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...
221
            }
222
        }
223
        return $result;
224
    }
225
226
    /**
227
     * Store an alias record in a list. If the alias record has
228
     * a known name, then the key of the list will be the record's name.
229
     * Otherwise, append the record to the end of the list with
230
     * a numeric index.
231
     *
232
     * @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...
233
     * @param AliasRecord $aliasRecord one more alias to store in the result
234
     */
235
    protected function storeAliasRecordInResut(&$result, AliasRecord $aliasRecord)
236
    {
237
        if (!$aliasRecord) {
238
            return;
239
        }
240
        $key = $aliasRecord->name();
241
        if (empty($key)) {
242
            $result[] = $aliasRecord;
243
            return;
244
        }
245
        $result[$key] = $aliasRecord;
246
    }
247
248
    /**
249
     * If the alias name is '@sitename', or if it is '@sitename.env', then
250
     * look for a sitename.site.yml file that contains it. We also handle
251
     * '@location.sitename.env' here as well.
252
     *
253
     * @param SiteAliasName $aliasName
254
     *
255
     * @return AliasRecord|false
256
     */
257
    protected function loadSingleAliasFile(SiteAliasName $aliasName)
258
    {
259
        // Check to see if the appropriate sitename.alias.yml file can be
260
        // found. Return if it cannot.
261
        $path = $this->discovery()
262
            ->filterByLocation($aliasName->location())
263
            ->findSingleSiteAliasFile($aliasName->sitename());
264
        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...
265
            return false;
266
        }
267
        return $this->loadSingleAliasFileWithNameAtPath($aliasName, $path);
268
    }
269
270
    /**
271
     * Given only the path to an alias file `site.alias.yml`, return all
272
     * of the alias records for every environment stored in that file.
273
     *
274
     * @param string $path
275
     * @return AliasRecord[]
276
     */
277
    protected function loadSingleSiteAliasFileAtPath($path)
278
    {
279
        $sitename = $this->siteNameFromPath($path);
280
        if ($siteData = $this->loadSiteDataFromPath($path)) {
281
            return $this->createAliasRecordsFromSiteData($sitename, $siteData);
282
        }
283
        return false;
284
    }
285
286
    /**
287
     * Given the path to a single site alias file `site.alias.yml`,
288
     * return the `site` part.
289
     *
290
     * @param string $path
291
     */
292
    protected function siteNameFromPath($path)
293
    {
294
        return $this->basenameWithoutExtension($path, '.site.yml');
295
296
// OR:
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% 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...
297
//        $filename = basename($path);
298
//        return preg_replace('#\..*##', '', $filename);
299
300
    }
301
302
    /**
303
     * Chop off the `aliases.yml` or `alias.yml` part of a path. This works
304
     * just like `basename`, except it will throw if the provided path
305
     * does not end in the specified extension.
306
     *
307
     * @param string $path
308
     * @param string $extension
309
     * @return string
310
     * @throws \Exception
311
     */
312
    protected function basenameWithoutExtension($path, $extension)
313
    {
314
        $result = basename($path, $extension);
315
        // It is an error if $path does not end with site.yml
316
        if ($result == basename($path)) {
317
            throw new \Exception("$path must end with '$extension'");
318
        }
319
        return $result;
320
    }
321
322
    /**
323
     * Given an alias name and a path, load the data from the path
324
     * and process it as needed to generate the alias record.
325
     *
326
     * @param SiteAliasName $aliasName
327
     * @param string $path
328
     * @return AliasRecord|false
329
     */
330
    protected function loadSingleAliasFileWithNameAtPath(SiteAliasName $aliasName, $path)
331
    {
332
        $data = $this->loadSiteDataFromPath($path);
333
        if (!$data) {
334
            return false;
335
        }
336
        $processor = new ConfigProcessor();
337
        return $this->fetchAliasRecordFromSiteAliasData($aliasName, $processor, $data);
338
    }
339
340
    /**
341
     * Load the yml from the given path
342
     *
343
     * @param string $path
344
     * @return array|bool
345
     */
346
    protected function loadSiteDataFromPath($path)
347
    {
348
        $data = $this->loadData($path);
349
        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...
350
            return false;
351
        }
352
        $selfSiteAliases = $this->findSelfSiteAliases($data);
353
        $data = array_merge($data, $selfSiteAliases);
354
        return $data;
355
    }
356
357
    /**
358
     * Given an array of site aliases, find the first one that is
359
     * local (has no 'host' item) and also contains a 'self.site.yml' file.
360
     * @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...
361
     * @return array
362
     */
363
    protected function findSelfSiteAliases($site_aliases)
364
    {
365
        foreach ($site_aliases as $site => $data) {
366
            if (!isset($data['host']) && isset($data['root'])) {
367
                foreach (['.', '..'] as $relative_path) {
368
                    $candidate = $data['root'] . '/' . $relative_path . '/drush/sites/self.site.yml';
369
                    if (file_exists($candidate)) {
370
                        return $this->loadData($candidate);
371
                    }
372
                }
373
            }
374
        }
375
        return [];
376
    }
377
378
    /**
379
     * Load the contents of the specified file.
380
     *
381
     * @param string $path Path to file to load
382
     * @return array
383
     */
384
    protected function loadData($path)
385
    {
386
        if (empty($path) || !file_exists($path)) {
387
            return [];
388
        }
389
        $loader = $this->getLoader(pathinfo($path, PATHINFO_EXTENSION));
390
        if (!$loader) {
391
            return [];
392
        }
393
        return $loader->load($path);
394
    }
395
396
    /**
397
     * @return DataFileLoaderInterface
398
     */
399
    public function getLoader($extension)
400
    {
401
        if (!isset($this->loader[$extension])) {
402
            return null;
403
        }
404
        return $this->loader[$extension];
405
    }
406
407
    public function addLoader($extension, DataFileLoaderInterface $loader)
408
    {
409
        $this->loader[$extension] = $loader;
410
    }
411
412
    /**
413
     * Given an array containing site alias data, return an alias record
414
     * containing the data for the requested record. If there is a 'common'
415
     * section, then merge that in as well.
416
     *
417
     * @param SiteAliasName $aliasName the alias we are loading
418
     * @param array $data
419
     *
420
     * @return AliasRecord|false
421
     */
422
    protected function fetchAliasRecordFromSiteAliasData(SiteAliasName $aliasName, ConfigProcessor $processor, array $data)
423
    {
424
        $data = $this->adjustIfSingleAlias($data);
425
        $env = $this->getEnvironmentName($aliasName, $data);
426
        if (!$this->siteEnvExists($data, $env)) {
427
            return false;
428
        }
429
430
        // Add the 'common' section if it exists.
431
        if (isset($data['common']) && is_array($data['common'])) {
432
            $processor->add($data['common']);
433
        }
434
435
        // Then add the data from the desired environment.
436
        $processor->add($data[$env]);
437
438
        // Export the combined data and create an AliasRecord object to manage it.
439
        return new AliasRecord($processor->export($this->referenceData), '@' . $aliasName->sitenameWithLocation(), $env);
440
    }
441
442
    /**
443
     * Determine whether there is a valid-looking environment '$env' in the
444
     * provided site alias data.
445
     *
446
     * @param array $data
447
     * @param string $env
448
     * @return bool
449
     */
450
    protected function siteEnvExists(array $data, $env)
451
    {
452
        return (
453
            is_array($data) &&
454
            isset($data[$env]) &&
455
            is_array($data[$env])
456
        );
457
    }
458
459
    /**
460
     * Adjust the alias data for a single-site alias. Usually, a .yml alias
461
     * file will contain multiple entries, one for each of the environments
462
     * of an alias. If there are no environments
463
     *
464
     * @param array $data
465
     * @return array
466
     */
467
    protected function adjustIfSingleAlias($data)
468
    {
469
        if (!$this->detectSingleAlias($data)) {
470
            return $data;
471
        }
472
473
        $result = [
474
            'default' => $data,
475
        ];
476
477
        return $result;
478
    }
479
480
    /**
481
     * A single-environment alias looks something like this:
482
     *
483
     *   ---
484
     *   root: /path/to/drupal
485
     *   uri: https://mysite.org
486
     *
487
     * A multiple-environment alias looks something like this:
488
     *
489
     *   ---
490
     *   default: dev
491
     *   dev:
492
     *     root: /path/to/dev
493
     *     uri: https://dev.mysite.org
494
     *   stage:
495
     *     root: /path/to/stage
496
     *     uri: https://stage.mysite.org
497
     *
498
     * The differentiator between these two is that the multi-environment
499
     * alias always has top-level elements that are associative arrays, and
500
     * the single-environment alias never does.
501
     *
502
     * @param array $data
503
     * @return bool
504
     */
505
    protected function detectSingleAlias($data)
506
    {
507
        foreach ($data as $key => $value) {
508
            if (is_array($value) && DotAccessDataUtil::isAssoc($value)) {
509
                return false;
510
            }
511
        }
512
        return true;
513
    }
514
515
    /**
516
     * Return the name of the environment requested.
517
     *
518
     * @param SiteAliasName $aliasName the alias we are loading
519
     * @param array $data
520
     *
521
     * @return string
522
     */
523
    protected function getEnvironmentName(SiteAliasName $aliasName, array $data)
524
    {
525
        // If the alias name specifically mentions the environment
526
        // to use, then return it.
527
        if ($aliasName->hasEnv()) {
528
            return $aliasName->env();
529
        }
530
        return $this->getDefaultEnvironmentName($data);
531
    }
532
533
    /**
534
     * Given a data array containing site alias environments, determine which
535
     * envirionmnet should be used as the default environment.
536
     *
537
     * @param array $data
538
     * @return string
539
     */
540
    protected function getDefaultEnvironmentName(array $data)
541
    {
542
        // If there is an entry named 'default', it will either contain the
543
        // name of the environment to use by default, or it will itself be
544
        // the default environment.
545
        if (isset($data['default'])) {
546
            return is_array($data['default']) ? 'default' : $data['default'];
547
        }
548
        // If there is an environment named 'dev', it will be our default.
549
        if (isset($data['dev'])) {
550
            return 'dev';
551
        }
552
        // If we don't know which environment to use, just take the first one.
553
        $keys = array_keys($data);
554
        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 554 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...
555
    }
556
}
557