Completed
Push — master ( 1d624e...5d3011 )
by Greg
01:28
created

createAliasRecordsFromSiteData()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 17
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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