Completed
Push — master ( a18f4a...b0c220 )
by Greg
01:31
created

SiteAliasFileLoader   C

Complexity

Total Complexity 68

Size/Duplication

Total Lines 505
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 68
lcom 1
cbo 6
dl 0
loc 505
rs 5.6756
c 0
b 0
f 0

27 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 2
A setReferenceData() 0 4 1
A addSearchLocation() 0 5 1
A discovery() 0 4 1
A load() 0 20 3
A loadDefaultEnvFromSitename() 0 16 3
A loadAll() 0 15 4
A listAll() 0 4 1
A loadMultiple() 0 10 3
A createAliasRecordsFromSiteData() 0 14 3
A storeAliasRecordInResut() 0 12 3
A loadSingleAliasFile() 0 10 2
A loadSingleSiteAliasFileAtPath() 0 8 2
A siteNameFromPath() 0 4 1
A basenameWithoutExtension() 0 9 2
A loadSingleAliasFileWithNameAtPath() 0 9 2
A loadSiteDataFromPath() 0 10 2
B findSelfSiteAliases() 0 14 6
A loadData() 0 11 4
A getLoader() 0 7 2
A addLoader() 0 4 1
A fetchAliasRecordFromSiteAliasData() 0 19 4
A siteEnvExists() 0 8 3
A adjustIfSingleAlias() 0 12 2
A detectSingleAlias() 0 9 4
A getEnvironmentName() 0 9 2
A getDefaultEnvironmentName() 0 16 4

How to fix   Complexity   

Complex Class

Complex classes like SiteAliasFileLoader often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SiteAliasFileLoader, and based on these observations, apply Extract Interface, too.

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` and defaulted to `@self.foo`,
0 ignored issues
show
Unused Code Comprehensibility introduced by
36% 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
        // then make a new alias name `@foo.default` and see if we can find that.
96
        // Note that at the moment, `foo` is stored in $aliasName->env().
97
        $sitename = $aliasName->env();
98
        return $this->loadDefaultEnvFromSitename($sitename);
99
    }
100
101
    /**
102
     * Given only a site name, load the default environment from it.
103
     */
104
    protected function loadDefaultEnvFromSitename($sitename)
105
    {
106
        $path = $this->discovery()->findSingleSiteAliasFile($sitename);
107
        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...
108
            return false;
109
        }
110
        $data = $this->loadSiteDataFromPath($path);
111
        if (!$data) {
112
            return false;
113
        }
114
        $env = $this->getDefaultEnvironmentName($data);
115
116
        $aliasName = new SiteAliasName($sitename, $env);
117
        $processor = new ConfigProcessor();
118
        return $this->fetchAliasRecordFromSiteAliasData($aliasName, $processor, $data);
119
    }
120
121
    /**
122
     * Return a list of all site aliases loadable from any findable path.
123
     *
124
     * @return AliasRecord[]
125
     */
126
    public function loadAll()
127
    {
128
        $result = [];
129
        $paths = $this->discovery()->findAllSingleAliasFiles();
130
        foreach ($paths as $path) {
131
            $aliasRecords = $this->loadSingleSiteAliasFileAtPath($path);
132
            if ($aliasRecords) {
133
                foreach ($aliasRecords as $aliasRecord) {
134
                    $this->storeAliasRecordInResut($result, $aliasRecord);
135
                }
136
            }
137
        }
138
        ksort($result);
139
        return $result;
140
    }
141
142
    /**
143
     * Return a list of all available alias files. Does not include
144
     * legacy files.
145
     *
146
     * @return string[]
147
     */
148
    public function listAll()
149
    {
150
        return $this->discovery()->findAllSingleAliasFiles();
151
    }
152
153
    /**
154
     * Given an alias name that might represent multiple sites,
155
     * return a list of all matching alias records. If nothing was found,
156
     * or the name represents a single site + env, then we take
157
     * no action and return `false`.
158
     *
159
     * @param string $sitename The site name to return all environments for.
160
     * @return AliasRecord[]|false
161
     */
162
    public function loadMultiple($sitename)
163
    {
164
        if ($path = $this->discovery()->findSingleSiteAliasFile($sitename)) {
165
            if ($siteData = $this->loadSiteDataFromPath($path)) {
166
                // Convert the raw array into a list of alias records.
167
                return $this->createAliasRecordsFromSiteData($sitename, $siteData);
168
            }
169
        }
170
        return false;
171
    }
172
173
    /**
174
     * @param array $siteData list of sites with its respective data
175
     *
176
     * @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...
177
     * @param $siteData An associative array of envrionment => site data
178
     * @return AliasRecord[]
179
     */
180
    protected function createAliasRecordsFromSiteData($sitename, $siteData)
181
    {
182
        $result = [];
183
        foreach ($siteData as $envName => $data) {
184
            if (is_array($data)) {
185
                $aliasName = new SiteAliasName($sitename, $envName);
186
187
                $processor = new ConfigProcessor();
188
                $oneRecord = $this->fetchAliasRecordFromSiteAliasData($aliasName, $processor, $siteData);
189
                $this->storeAliasRecordInResut($result, $oneRecord);
0 ignored issues
show
Security Bug introduced by
It seems like $oneRecord defined by $this->fetchAliasRecordF... $processor, $siteData) on line 188 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...
190
            }
191
        }
192
        return $result;
193
    }
194
195
    /**
196
     * Store an alias record in a list. If the alias record has
197
     * a known name, then the key of the list will be the record's name.
198
     * Otherwise, append the record to the end of the list with
199
     * a numeric index.
200
     *
201
     * @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...
202
     * @param AliasRecord $aliasRecord one more alias to store in the result
203
     */
204
    protected function storeAliasRecordInResut(&$result, AliasRecord $aliasRecord)
205
    {
206
        if (!$aliasRecord) {
207
            return;
208
        }
209
        $key = $aliasRecord->name();
210
        if (empty($key)) {
211
            $result[] = $aliasRecord;
212
            return;
213
        }
214
        $result[$key] = $aliasRecord;
215
    }
216
217
    /**
218
     * If the alias name is '@sitename', or if it is '@sitename.env', then
219
     * look for a sitename.site.yml file that contains it.
220
     *
221
     * @param SiteAliasName $aliasName
222
     *
223
     * @return AliasRecord|false
224
     */
225
    protected function loadSingleAliasFile(SiteAliasName $aliasName)
226
    {
227
        // Check to see if the appropriate sitename.alias.yml file can be
228
        // found. Return if it cannot.
229
        $path = $this->discovery()->findSingleSiteAliasFile($aliasName->sitename());
230
        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...
231
            return false;
232
        }
233
        return $this->loadSingleAliasFileWithNameAtPath($aliasName, $path);
234
    }
235
236
    /**
237
     * Given only the path to an alias file `site.alias.yml`, return all
238
     * of the alias records for every environment stored in that file.
239
     *
240
     * @param string $path
241
     * @return AliasRecord[]
242
     */
243
    protected function loadSingleSiteAliasFileAtPath($path)
244
    {
245
        $sitename = $this->siteNameFromPath($path);
246
        if ($siteData = $this->loadSiteDataFromPath($path)) {
247
            return $this->createAliasRecordsFromSiteData($sitename, $siteData);
248
        }
249
        return false;
250
    }
251
252
    /**
253
     * Given the path to a single site alias file `site.alias.yml`,
254
     * return the `site` part.
255
     *
256
     * @param string $path
257
     */
258
    protected function siteNameFromPath($path)
259
    {
260
        return $this->basenameWithoutExtension($path, '.site.yml');
261
    }
262
263
    /**
264
     * Chop off the `aliases.yml` or `alias.yml` part of a path. This works
265
     * just like `basename`, except it will throw if the provided path
266
     * does not end in the specified extension.
267
     *
268
     * @param string $path
269
     * @param string $extension
270
     * @return string
271
     * @throws \Exception
272
     */
273
    protected function basenameWithoutExtension($path, $extension)
274
    {
275
        $result = basename($path, $extension);
276
        // It is an error if $path does not end with site.yml
277
        if ($result == basename($path)) {
278
            throw new \Exception("$path must end with '$extension'");
279
        }
280
        return $result;
281
    }
282
283
    /**
284
     * Given an alias name and a path, load the data from the path
285
     * and process it as needed to generate the alias record.
286
     *
287
     * @param SiteAliasName $aliasName
288
     * @param string $path
289
     * @return AliasRecord|false
290
     */
291
    protected function loadSingleAliasFileWithNameAtPath(SiteAliasName $aliasName, $path)
292
    {
293
        $data = $this->loadSiteDataFromPath($path);
294
        if (!$data) {
295
            return false;
296
        }
297
        $processor = new ConfigProcessor();
298
        return $this->fetchAliasRecordFromSiteAliasData($aliasName, $processor, $data);
299
    }
300
301
    /**
302
     * Load the yml from the given path
303
     *
304
     * @param string $path
305
     * @return array|bool
306
     */
307
    protected function loadSiteDataFromPath($path)
308
    {
309
        $data = $this->loadData($path);
310
        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...
311
            return false;
312
        }
313
        $selfSiteAliases = $this->findSelfSiteAliases($data);
314
        $data = array_merge($data, $selfSiteAliases);
315
        return $data;
316
    }
317
318
    /**
319
     * Given an array of site aliases, find the first one that is
320
     * local (has no 'host' item) and also contains a 'self.site.yml' file.
321
     * @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...
322
     * @return array
323
     */
324
    protected function findSelfSiteAliases($site_aliases)
325
    {
326
        foreach ($site_aliases as $site => $data) {
327
            if (!isset($data['host']) && isset($data['root'])) {
328
                foreach (['.', '..'] as $relative_path) {
329
                    $candidate = $data['root'] . '/' . $relative_path . '/drush/sites/self.site.yml';
330
                    if (file_exists($candidate)) {
331
                        return $this->loadData($candidate);
332
                    }
333
                }
334
            }
335
        }
336
        return [];
337
    }
338
339
    /**
340
     * Load the contents of the specified file.
341
     *
342
     * @param string $path Path to file to load
343
     * @return array
344
     */
345
    protected function loadData($path)
346
    {
347
        if (empty($path) || !file_exists($path)) {
348
            return [];
349
        }
350
        $loader = $this->getLoader(pathinfo($path, PATHINFO_EXTENSION));
351
        if (!$loader) {
352
            return [];
353
        }
354
        return $loader->load($path);
355
    }
356
357
    /**
358
     * @return DataFileLoaderInterface
359
     */
360
    public function getLoader($extension)
361
    {
362
        if (!isset($this->loader[$extension])) {
363
            return null;
364
        }
365
        return $this->loader[$extension];
366
    }
367
368
    public function addLoader($extension, DataFileLoaderInterface $loader)
369
    {
370
        $this->loader[$extension] = $loader;
371
    }
372
373
    /**
374
     * Given an array containing site alias data, return an alias record
375
     * containing the data for the requested record. If there is a 'common'
376
     * section, then merge that in as well.
377
     *
378
     * @param SiteAliasName $aliasName the alias we are loading
379
     * @param array $data
380
     *
381
     * @return AliasRecord|false
382
     */
383
    protected function fetchAliasRecordFromSiteAliasData(SiteAliasName $aliasName, ConfigProcessor $processor, array $data)
384
    {
385
        $data = $this->adjustIfSingleAlias($data);
386
        $env = $this->getEnvironmentName($aliasName, $data);
387
        if (!$this->siteEnvExists($data, $env)) {
388
            return false;
389
        }
390
391
        // Add the 'common' section if it exists.
392
        if (isset($data['common']) && is_array($data['common'])) {
393
            $processor->add($data['common']);
394
        }
395
396
        // Then add the data from the desired environment.
397
        $processor->add($data[$env]);
398
399
        // Export the combined data and create an AliasRecord object to manage it.
400
        return new AliasRecord($processor->export($this->referenceData), '@' . $aliasName->sitename(), $env);
401
    }
402
403
    /**
404
     * Determine whether there is a valid-looking environment '$env' in the
405
     * provided site alias data.
406
     *
407
     * @param array $data
408
     * @param string $env
409
     * @return bool
410
     */
411
    protected function siteEnvExists(array $data, $env)
412
    {
413
        return (
414
            is_array($data) &&
415
            isset($data[$env]) &&
416
            is_array($data[$env])
417
        );
418
    }
419
420
    /**
421
     * Adjust the alias data for a single-site alias. Usually, a .yml alias
422
     * file will contain multiple entries, one for each of the environments
423
     * of an alias. If there are no environments
424
     *
425
     * @param array $data
426
     * @return array
427
     */
428
    protected function adjustIfSingleAlias($data)
429
    {
430
        if (!$this->detectSingleAlias($data)) {
431
            return $data;
432
        }
433
434
        $result = [
435
            'default' => $data,
436
        ];
437
438
        return $result;
439
    }
440
441
    /**
442
     * A single-environment alias looks something like this:
443
     *
444
     *   ---
445
     *   root: /path/to/drupal
446
     *   uri: https://mysite.org
447
     *
448
     * A multiple-environment alias looks something like this:
449
     *
450
     *   ---
451
     *   default: dev
452
     *   dev:
453
     *     root: /path/to/dev
454
     *     uri: https://dev.mysite.org
455
     *   stage:
456
     *     root: /path/to/stage
457
     *     uri: https://stage.mysite.org
458
     *
459
     * The differentiator between these two is that the multi-environment
460
     * alias always has top-level elements that are associative arrays, and
461
     * the single-environment alias never does.
462
     *
463
     * @param array $data
464
     * @return bool
465
     */
466
    protected function detectSingleAlias($data)
467
    {
468
        foreach ($data as $key => $value) {
469
            if (is_array($value) && DotAccessDataUtil::isAssoc($value)) {
470
                return false;
471
            }
472
        }
473
        return true;
474
    }
475
476
    /**
477
     * Return the name of the environment requested.
478
     *
479
     * @param SiteAliasName $aliasName the alias we are loading
480
     * @param array $data
481
     *
482
     * @return string
483
     */
484
    protected function getEnvironmentName(SiteAliasName $aliasName, array $data)
485
    {
486
        // If the alias name specifically mentions the environment
487
        // to use, then return it.
488
        if ($aliasName->hasEnv()) {
489
            return $aliasName->env();
490
        }
491
        return $this->getDefaultEnvironmentName($data);
492
    }
493
494
    /**
495
     * Given a data array containing site alias environments, determine which
496
     * envirionmnet should be used as the default environment.
497
     *
498
     * @param array $data
499
     * @return string
500
     */
501
    protected function getDefaultEnvironmentName(array $data)
502
    {
503
        // If there is an entry named 'default', it will either contain the
504
        // name of the environment to use by default, or it will itself be
505
        // the default environment.
506
        if (isset($data['default'])) {
507
            return is_array($data['default']) ? 'default' : $data['default'];
508
        }
509
        // If there is an environment named 'dev', it will be our default.
510
        if (isset($data['dev'])) {
511
            return 'dev';
512
        }
513
        // If we don't know which environment to use, just take the first one.
514
        $keys = array_keys($data);
515
        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 515 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...
516
    }
517
}
518