Completed
Push — integration-tests ( 50e409...2f2012 )
by Cy
02:18
created

Loader::getPackageConfigurationFor()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 8
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 4
nc 2
nop 1
1
<?php
2
3
namespace Monospice\LaravelRedisSentinel\Configuration;
4
5
use Illuminate\Foundation\Application as LaravelApplication;
6
use Illuminate\Support\Arr;
7
use Laravel\Horizon\Horizon;
8
use Laravel\Lumen\Application as LumenApplication;
9
use Monospice\LaravelRedisSentinel\Configuration\HostNormalizer;
10
use Monospice\LaravelRedisSentinel\Manager;
11
use UnexpectedValueException;
12
13
/**
14
 * The internal configuration loader for the package. Used by the package's
15
 * service provider.
16
 *
17
 * This package provides developers three ways to configure it: through the
18
 * environment, by adding config values to the configuration for the other
19
 * components that the package wraps, and by creating an external package
20
 * configuration file that overrides the default internal configuration.
21
 * The package uses its configuration information to set Redis connection,
22
 * cache, session, and queue configuration values when these are missing.
23
 * This approach simplifies the code needed to configure the package for many
24
 * applications while still providing the flexibility needed for advanced
25
 * setups. This class reconciles each of the configuration methods.
26
 *
27
 * The package's configuration contains partial elements from several other
28
 * component configurations. By default, the package removes its configuration
29
 * after merging the values into each of the appropriate config locations for
30
 * the components it initializes. This behavior prevents the artisan CLI's
31
 * "config:cache" command from saving unnecessary configuration values to the
32
 * configuration cache file. Set the value of "redis-sentinel.clean_config" to
33
 * FALSE to disable this behavior.
34
 *
35
 * To support these configuration scenarios, this class follows these rules:
36
 *
37
 *   - Values in application config files ("config/database.php", etc.) have
38
 *     the greatest precedence. The package will use these values before any
39
 *     others and will not modify these values if they exist.
40
 *   - The package will use values in a developer-supplied package config file
41
 *     located in the application's "config/" directory with the filename of
42
 *     "redis-sentinel.php" for any values not found in the application's
43
 *     standard configuration files before using it's default configuration.
44
 *   - For any configuration values not provided by standard application
45
 *     config files or a developer-supplied custom config file, the package
46
 *     uses it's internal default configuration that reads configuration values
47
 *     from environment variables.
48
 *   - The package will copy values from it's configuration to the standard
49
 *     application configuration at runtime if these are missing. For example,
50
 *     if the application configuration doesn't contain a key for "database.
51
 *     redis-sentinel" (the Redis Sentinel connections), this class will copy
52
 *     its values from "redis-sentinel.database.redis-sentinel" to "database.
53
 *     redis-sentinel".
54
 *   - After loading its configuration, the package must only use configuration
55
 *     values from the standard application config locations. For example, the
56
 *     package will read the values from "database.redis-sentinel" to configure
57
 *     Redis Sentinel connections, not "redis-sentinel.database.redis-sentinel".
58
 *
59
 * @category Package
60
 * @package  Monospice\LaravelRedisSentinel
61
 * @author   Cy Rossignol <[email protected]>
62
 * @license  See LICENSE file
63
 * @link     https://github.com/monospice/laravel-redis-sentinel-drivers
64
 */
65
class Loader
66
{
67
    /**
68
     * The path to the package's default configuration file.
69
     *
70
     * @var string
71
     */
72
    const CONFIG_PATH = __DIR__ . '/../../config/redis-sentinel.php';
73
74
    /**
75
     * Flag that indicates whether the package should check that the framework
76
     * runs full Laravel before loading Horizon support.
77
     *
78
     * Horizon does not yet officially support Lumen applications, so the
79
     * package will not configure itself for Horizon in Lumen applications by
80
     * default. Set this value to TRUE in bootstrap/app.php to short-circuit
81
     * the check and attempt to load Horizon support anyway. This provides for
82
     * testing unofficial Lumen implementations. Use at your own risk.
83
     *
84
     * @var bool
85
     */
86
    public static $ignoreHorizonRequirements = false;
87
88
    /**
89
     * Indicates whether the current application runs the Lumen framework.
90
     *
91
     * @var bool
92
     */
93
    public $isLumen;
94
95
    /**
96
     * Indicates whether the current application supports sessions.
97
     *
98
     * @var bool
99
     */
100
    public $supportsSessions;
101
102
    /**
103
     * Indicates whether the package should override Laravel's standard Redis
104
     * API ("Redis" facade and "redis" service binding).
105
     *
106
     * @var bool
107
     */
108
    public $shouldOverrideLaravelRedisApi;
109
110
    /**
111
     * Indicates whether Laravel Horizon is installed. Currently FALSE in Lumen.
112
     *
113
     * @var bool
114
     */
115
    public $horizonAvailable;
116
117
    /**
118
     * Indicates whether the package should integrate with Laravel Horizon
119
     * based on availability and the value of the "horizon.driver" directive.
120
     *
121
     * @var bool
122
     */
123
    public $shouldIntegrateHorizon;
124
125
    /**
126
     * The current Laravel or Lumen application instance that provides context
127
     * and services used to load the appropriate configuration.
128
     *
129
     * @var LaravelApplication|LumenApplication
130
     */
131
    private $app;
132
133
    /**
134
     * Used to fetch and set application configuration values.
135
     *
136
     * @var \Illuminate\Contracts\Config\Repository
137
     */
138
    private $config;
139
140
    /**
141
     * Contains the set of configuration values used to configure the package
142
     * as loaded from "config/redis-sentinel.php". Empty when the application's
143
     * standard config files provide all the values needed to configure the
144
     * package (such as when a developer provides a custom config).
145
     *
146
     * @var array
147
     */
148
    private $packageConfig;
149
150
    /**
151
     * Initialize the configuration loader. Any actual loading occurs when
152
     * calling the 'loadConfiguration()' method.
153
     *
154
     * @param LaravelApplication|LumenApplication $app The current application
155
     * instance that provides context and services needed to load the
156
     * appropriate configuration.
157
     */
158
    public function __construct($app)
159
    {
160
        $this->app = $app;
161
        $this->config = $app->make('config');
162
163
        $lumenApplicationClass = 'Laravel\Lumen\Application';
164
165
        $this->isLumen = $app instanceof $lumenApplicationClass;
166
        $this->supportsSessions = $app->bound('session');
167
        $this->horizonAvailable = static::$ignoreHorizonRequirements
168
            || ! $this->isLumen && class_exists(Horizon::class);
169
    }
170
171
    /**
172
     * Create an instance of the loader and load the configuration in one step.
173
     *
174
     * @param LaravelApplication|LumenApplication $app The current application
175
     * instance that provides context and services needed to load the
176
     * appropriate configuration.
177
     *
178
     * @return self An initialized instance of this class
179
     */
180
    public static function load($app)
181
    {
182
        $loader = new self($app);
183
        $loader->loadConfiguration();
184
185
        return $loader;
186
    }
187
188
    /**
189
     * Load the package configuration.
190
     *
191
     * @return void
192
     */
193
    public function loadConfiguration()
194
    {
195
        if ($this->shouldLoadConfiguration()) {
196
            if ($this->isLumen) {
197
                $this->configureLumenComponents();
198
            }
199
200
            $this->loadPackageConfiguration();
201
        }
202
203
        // Previous versions of the package looked for the value 'sentinel':
204
        $redisDriver = $this->config->get('database.redis.driver');
205
        $this->shouldOverrideLaravelRedisApi = $redisDriver === 'redis-sentinel'
206
            || $redisDriver === 'sentinel';
207
208
        $this->shouldIntegrateHorizon = $this->horizonAvailable
209
           && $this->config->get('horizon.driver') === 'redis-sentinel';
210
    }
211
212
    /**
213
     * Sets the Horizon Redis Sentinel connection configuration.
214
     *
215
     * @return void
216
     */
217
    public function loadHorizonConfiguration()
218
    {
219
        // We set the config value "redis-sentinel.load_horizon" to FALSE after
220
        // configuring Horizon connections to skip this step after caching the
221
        // application configuration via "artisan config:cache":
222
        if ($this->config->get('redis-sentinel.load_horizon', true) !== true) {
223
            return;
224
        }
225
226
        $horizonConfig = $this->getSelectedHorizonConnectionConfiguration();
227
        $options = Arr::get($horizonConfig, 'options', [ ]);
228
        $options['prefix'] = $this->config->get('horizon.prefix', 'horizon:');
229
230
        $horizonConfig['options'] = $options;
231
232
        $this->config->set('database.redis-sentinel.horizon', $horizonConfig);
233
        $this->config->set('redis-sentinel.load_horizon', false);
234
    }
235
236
    /**
237
     * Get the version number of the current Laravel or Lumen application.
238
     *
239
     * @return string The version as declared by the framework.
240
     */
241
    public function getApplicationVersion()
242
    {
243
        if ($this->isLumen) {
244
            return substr($this->app->version(), 7, 3); // ex. "5.4"
245
        }
246
247
        return  \Illuminate\Foundation\Application::VERSION;
248
    }
249
250
    /**
251
     * Fetch the specified application configuration value.
252
     *
253
     * This helper method enables the package's service providers to get config
254
     * values without having to resolve the config service from the container.
255
     *
256
     * @param string|array $key     The key(s) for the value(s) to fetch.
257
     * @param mixed        $default Returned if the key does not exist.
258
     *
259
     * @return mixed The requested configuration value or the provided default
260
     * if the key does not exist.
261
     */
262
    public function get($key, $default = null)
263
    {
264
        return $this->config->get($key, $default);
265
    }
266
267
    /**
268
     * Set the specified application configuration value.
269
     *
270
     * This helper method enables the package's service providers to set config
271
     * values without having to resolve the config service from the container.
272
     *
273
     * @param string|array $key   The key of the value or a tree of values as
274
     * an associative array.
275
     * @param mixed        $value The value to set for the specified key.
276
     *
277
     * @return void
278
     */
279
    public function set($key, $value = null)
280
    {
281
        $this->config->set($key, $value);
282
    }
283
284
    /**
285
     * Determine if the package should automatically configure itself.
286
     *
287
     * Developers may set the value of "redis-sentinel.load_config" to FALSE to
288
     * disable the package's automatic configuration. This class also sets this
289
     * value to FALSE after loading the package configuration to skip the auto-
290
     * configuration when the application cached its configuration values (via
291
     * "artisan config:cache", for example).
292
     *
293
     * @return bool TRUE if the package should load its configuration
294
     */
295
    protected function shouldLoadConfiguration()
296
    {
297
        if ($this->isLumen) {
298
            $this->app->configure('redis-sentinel');
299
        }
300
301
        return $this->config->get('redis-sentinel.load_config', true) === true;
302
    }
303
304
    /**
305
     * Configure the Lumen components that this package depends on.
306
     *
307
     * Lumen lazily loads many of its components. We must instruct Lumen to
308
     * load the configuration for components that this class configures so
309
     * that the values are accessible and so that the framework does not
310
     * revert the configuration settings that this class changes when one of
311
     * the components initializes later.
312
     *
313
     * @return void
314
     */
315
    protected function configureLumenComponents()
316
    {
317
        $this->app->configure('database');
318
        $this->app->configure('broadcasting');
319
        $this->app->configure('cache');
320
        $this->app->configure('queue');
321
    }
322
323
    /**
324
     * Copy the Redis Sentinel connection configuration to use for Horizon
325
     * connections from the connection specified by "horizon.use".
326
     *
327
     * @return array The configuration matching the connection name specified
328
     * by the "horizon.use" config directive.
329
     *
330
     * @throws UnexpectedValueException If no Redis Sentinel connection matches
331
     * the name declared by "horizon.use".
332
     */
333
    protected function getSelectedHorizonConnectionConfiguration()
334
    {
335
        $use = $this->config->get('horizon.use', 'default');
336
        $connectionConfig = $this->config->get("database.redis-sentinel.$use");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $use instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
337
338
        if ($connectionConfig === null) {
339
            throw new UnexpectedValueException(
340
                "The Horizon Redis Sentinel connection [$use] is not defined."
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $use instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
341
            );
342
        }
343
344
        return $connectionConfig;
345
    }
346
347
    /**
348
     * Reconcile the package configuration and use it to set the appropriate
349
     * configuration values for other application components.
350
     *
351
     * @return void
352
     */
353
    protected function loadPackageConfiguration()
354
    {
355
        $this->setConfigurationFor('database.redis-sentinel');
356
        $this->setConfigurationFor('database.redis.driver');
357
        $this->setConfigurationFor('broadcasting.connections.redis-sentinel');
358
        $this->setConfigurationFor('cache.stores.redis-sentinel');
359
        $this->setConfigurationFor('queue.connections.redis-sentinel');
360
        $this->setSessionConfiguration();
361
362
        $this->normalizeHosts();
363
364
        if ($this->packageConfig !== null) {
365
            $this->cleanPackageConfiguration();
366
        }
367
368
        // Skip loading package config when cached:
369
        $this->config->set('redis-sentinel.load_config', false);
370
    }
371
372
    /**
373
     * Set the application configuration value for the specified key with the
374
     * value from the package configuration.
375
     *
376
     * @param string $configKey   The key of the config value to set. Should
377
     * correspond to a key in the package's configuration.
378
     * @param bool   $checkExists If TRUE, don't set the value if the key
379
     * already exists in the application configuration.
380
     *
381
     * @return void
382
     */
383
    protected function setConfigurationFor($configKey, $checkExists = true)
384
    {
385
        if ($checkExists && $this->config->has($configKey)) {
386
            return;
387
        }
388
389
        $config = $this->getPackageConfigurationFor($configKey);
390
391
        $this->config->set($configKey, $config);
392
    }
393
394
    /**
395
     * Set the application session configuration as specified by the package's
396
     * configuration if the app supports sessions.
397
     *
398
     * @return void
399
     */
400
    protected function setSessionConfiguration()
401
    {
402
        if (! $this->supportsSessions
403
            || $this->config->get('session.driver') !== 'redis-sentinel'
404
            || $this->config->get('session.connection') !== null
405
        ) {
406
            return;
407
        }
408
409
        $this->setConfigurationFor('session.connection', false);
410
    }
411
412
    /**
413
     * Get the package configuration for the specified key.
414
     *
415
     * @param string $configKey The key of the configuration value to get
416
     *
417
     * @return mixed The value of the configuration with the specified key
418
     */
419
    protected function getPackageConfigurationFor($configKey)
420
    {
421
        if ($this->packageConfig === null) {
422
            $this->mergePackageConfiguration();
423
        }
424
425
        return Arr::get($this->packageConfig, $configKey);
426
    }
427
428
    /**
429
     * Merge the package's default configuration with the override config file
430
     * supplied by the developer, if any.
431
     *
432
     * @return void
433
     */
434
    protected function mergePackageConfiguration()
435
    {
436
        $defaultConfig = require self::CONFIG_PATH;
437
        $currentConfig = $this->config->get('redis-sentinel', [ ]);
438
439
        $this->packageConfig = array_merge($defaultConfig, $currentConfig);
440
    }
441
442
    /**
443
     * Parse Redis Sentinel connection host definitions to create single host
444
     * entries for host definitions that specify multiple hosts.
445
     *
446
     * @return void
447
     */
448
    protected function normalizeHosts()
449
    {
450
        $connections = $this->config->get('database.redis-sentinel');
451
452
        if (! is_array($connections)) {
453
            return;
454
        }
455
456
        $this->config->set(
457
            'database.redis-sentinel',
458
            HostNormalizer::normalizeConnections($connections)
459
        );
460
    }
461
462
    /**
463
     * Remove the package's configuration from the application configuration
464
     * repository.
465
     *
466
     * This package's configuration contains partial elements from several
467
     * other component configurations. By default, the package removes its
468
     * configuration after merging the values into each of the appropriate
469
     * config locations for the components it initializes. This behavior
470
     * prevents the artisan "config:cache" command from saving unnecessary
471
     * configuration values to the cache file.
472
     *
473
     * @return void
474
     */
475
    protected function cleanPackageConfiguration()
476
    {
477
        // When we're finished with the internal package configuration, break
478
        // the reference so that it can be garbage-collected:
479
        $this->packageConfig = null;
480
481
        if ($this->config->get('redis-sentinel.clean_config', true) !== true) {
482
            return;
483
        }
484
485
        $this->config->set('redis-sentinel', [
486
            'Config merged. Set "redis-sentinel.clean_config" = false to keep.',
487
        ]);
488
    }
489
}
490