Completed
Push — master ( c869c4...b8996b )
by Greg
01:25
created

SiteAliasFileLoader::isValidEnvName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
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`
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 View Code Duplication
    public function loadMultiple($sitename, $location = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
168
    {
169
        $result = [];
170
        foreach ($this->discovery()->filterByLocation($location)->find($sitename) as $path) {
171
            if ($siteData = $this->loadSiteDataFromPath($path)) {
172
                $location = SiteAliasName::locationFromPath($path);
0 ignored issues
show
Documentation introduced by
$path is of type string, but the function expects a object<Consolidation\SiteAlias\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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 View Code Duplication
    public function loadLocation($location)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
190
    {
191
        $result = [];
192
        foreach ($this->listAll($location) as $path) {
193
            if ($siteData = $this->loadSiteDataFromPath($path)) {
194
                $location = SiteAliasName::locationFromPath($path);
0 ignored issues
show
Documentation introduced by
$path is of type string, but the function expects a object<Consolidation\SiteAlias\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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
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...
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);
0 ignored issues
show
Security Bug introduced by
It seems like $oneRecord defined by $this->fetchAliasRecordF... $processor, $siteData) on line 224 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...
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
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...
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) {
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...
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);
0 ignored issues
show
Documentation introduced by
$path is of type string, but the function expects a object<Consolidation\SiteAlias\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
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) {
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...
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
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...
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);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression reset($keys); of type string|false adds false to the return on line 594 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...
595
    }
596
}
597