Completed
Push — 2.x ( ed04a7...fe437f )
by Cy
01:45
created

Loader::getApplicationVersion()   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 0
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
            $this->loadPackageConfiguration();
197
        }
198
199
        // Previous versions of the package looked for the value 'sentinel':
200
        $redisDriver = $this->config->get('database.redis.driver');
201
        $this->shouldOverrideLaravelRedisApi = $redisDriver === 'redis-sentinel'
202
            || $redisDriver === 'sentinel';
203
204
        $this->shouldIntegrateHorizon = $this->horizonAvailable
205
            && $this->config->get('horizon.driver') === 'redis-sentinel';
206
    }
207
208
    /**
209
     * Sets the Horizon Redis Sentinel connection configuration.
210
     *
211
     * @return void
212
     */
213
    public function loadHorizonConfiguration()
214
    {
215
        // We set the config value "redis-sentinel.load_horizon" to FALSE after
216
        // configuring Horizon connections to skip this step after caching the
217
        // application configuration via "artisan config:cache":
218
        if ($this->config->get('redis-sentinel.load_horizon', true) !== true) {
219
            return;
220
        }
221
222
        $horizonConfig = $this->getSelectedHorizonConnectionConfiguration();
223
        $options = Arr::get($horizonConfig, 'options', [ ]);
224
        $options['prefix'] = $this->config->get('horizon.prefix', 'horizon:');
225
226
        $horizonConfig['options'] = $options;
227
228
        $this->config->set('database.redis-sentinel.horizon', $horizonConfig);
229
        $this->config->set('redis-sentinel.load_horizon', false);
230
    }
231
232
    /**
233
     * Get the version number of the current Laravel or Lumen application.
234
     *
235
     * @return string The version as declared by the framework.
236
     */
237
    public function getApplicationVersion()
238
    {
239
        if ($this->isLumen) {
240
            return substr($this->app->version(), 7, 3); // ex. "5.4"
241
        }
242
243
        return \Illuminate\Foundation\Application::VERSION;
244
    }
245
246
    /**
247
     * Fetch the specified application configuration value.
248
     *
249
     * This helper method enables the package's service providers to get config
250
     * values without having to resolve the config service from the container.
251
     *
252
     * @param string|array $key     The key(s) for the value(s) to fetch.
253
     * @param mixed        $default Returned if the key does not exist.
254
     *
255
     * @return mixed The requested configuration value or the provided default
256
     * if the key does not exist.
257
     */
258
    public function get($key, $default = null)
259
    {
260
        return $this->config->get($key, $default);
261
    }
262
263
    /**
264
     * Set the specified application configuration value.
265
     *
266
     * This helper method enables the package's service providers to set config
267
     * values without having to resolve the config service from the container.
268
     *
269
     * @param string|array $key   The key of the value or a tree of values as
270
     * an associative array.
271
     * @param mixed        $value The value to set for the specified key.
272
     *
273
     * @return void
274
     */
275
    public function set($key, $value = null)
276
    {
277
        $this->config->set($key, $value);
278
    }
279
280
    /**
281
     * Determine if the package should automatically configure itself.
282
     *
283
     * Developers may set the value of "redis-sentinel.load_config" to FALSE to
284
     * disable the package's automatic configuration. This class also sets this
285
     * value to FALSE after loading the package configuration to skip the auto-
286
     * configuration when the application cached its configuration values (via
287
     * "artisan config:cache", for example).
288
     *
289
     * @return bool TRUE if the package should load its configuration
290
     */
291
    protected function shouldLoadConfiguration()
292
    {
293
        if ($this->isLumen) {
294
            $this->app->configure('redis-sentinel');
295
        }
296
297
        return $this->config->get('redis-sentinel.load_config', true) === true;
298
    }
299
300
    /**
301
     * Configure the Lumen components that this package depends on.
302
     *
303
     * Lumen lazily loads many of its components. We must instruct Lumen to
304
     * load the configuration for components that this class configures so
305
     * that the values are accessible and so that the framework does not
306
     * revert the configuration settings that this class changes when one of
307
     * the components initializes later.
308
     *
309
     * @return void
310
     */
311
    protected function configureLumenComponents()
312
    {
313
        $this->app->configure('database');
314
        $this->app->configure('broadcasting');
315
        $this->app->configure('cache');
316
        $this->app->configure('queue');
317
    }
318
319
    /**
320
     * Copy the Redis Sentinel connection configuration to use for Horizon
321
     * connections from the connection specified by "horizon.use".
322
     *
323
     * @return array The configuration matching the connection name specified
324
     * by the "horizon.use" config directive.
325
     *
326
     * @throws UnexpectedValueException If no Redis Sentinel connection matches
327
     * the name declared by "horizon.use".
328
     */
329
    protected function getSelectedHorizonConnectionConfiguration()
330
    {
331
        $use = $this->config->get('horizon.use', 'default');
332
        $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...
333
334
        if ($connectionConfig === null) {
335
            throw new UnexpectedValueException(
336
                "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...
337
            );
338
        }
339
340
        return $connectionConfig;
341
    }
342
343
    /**
344
     * Reconcile the package configuration and use it to set the appropriate
345
     * configuration values for other application components.
346
     *
347
     * @return void
348
     */
349
    protected function loadPackageConfiguration()
350
    {
351
        if ($this->isLumen) {
352
            $this->configureLumenComponents();
353
        }
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
        $this->cleanPackageConfiguration();
365
    }
366
367
    /**
368
     * Set the application configuration value for the specified key with the
369
     * value from the package configuration.
370
     *
371
     * @param string $configKey   The key of the config value to set. Should
372
     * correspond to a key in the package's configuration.
373
     * @param bool   $checkExists If TRUE, don't set the value if the key
374
     * already exists in the application configuration.
375
     *
376
     * @return void
377
     */
378
    protected function setConfigurationFor($configKey, $checkExists = true)
379
    {
380
        if ($checkExists && $this->config->has($configKey)) {
381
            return;
382
        }
383
384
        $config = $this->getPackageConfigurationFor($configKey);
385
386
        $this->config->set($configKey, $config);
387
    }
388
389
    /**
390
     * Set the application session configuration as specified by the package's
391
     * configuration if the app supports sessions.
392
     *
393
     * @return void
394
     */
395
    protected function setSessionConfiguration()
396
    {
397
        if (! $this->supportsSessions
398
            || $this->config->get('session.driver') !== 'redis-sentinel'
399
            || $this->config->get('session.connection') !== null
400
        ) {
401
            return;
402
        }
403
404
        $this->setConfigurationFor('session.connection', false);
405
    }
406
407
    /**
408
     * Get the package configuration for the specified key.
409
     *
410
     * @param string $configKey The key of the configuration value to get
411
     *
412
     * @return mixed The value of the configuration with the specified key
413
     */
414
    protected function getPackageConfigurationFor($configKey)
415
    {
416
        if ($this->packageConfig === null) {
417
            $this->mergePackageConfiguration();
418
        }
419
420
        return Arr::get($this->packageConfig, $configKey);
421
    }
422
423
    /**
424
     * Merge the package's default configuration with the override config file
425
     * supplied by the developer, if any.
426
     *
427
     * @return void
428
     */
429
    protected function mergePackageConfiguration()
430
    {
431
        $defaultConfig = require self::CONFIG_PATH;
432
        $currentConfig = $this->config->get('redis-sentinel', [ ]);
433
434
        $this->packageConfig = array_merge($defaultConfig, $currentConfig);
435
    }
436
437
    /**
438
     * Parse Redis Sentinel connection host definitions to create single host
439
     * entries for host definitions that specify multiple hosts.
440
     *
441
     * @return void
442
     */
443
    protected function normalizeHosts()
444
    {
445
        $connections = $this->config->get('database.redis-sentinel');
446
447
        if (! is_array($connections)) {
448
            return;
449
        }
450
451
        $this->config->set(
452
            'database.redis-sentinel',
453
            HostNormalizer::normalizeConnections($connections)
454
        );
455
    }
456
457
    /**
458
     * Remove the package's configuration from the application configuration
459
     * repository.
460
     *
461
     * This package's configuration contains partial elements from several
462
     * other component configurations. By default, the package removes its
463
     * configuration after merging the values into each of the appropriate
464
     * config locations for the components it initializes. This behavior
465
     * prevents the artisan "config:cache" command from saving unnecessary
466
     * configuration values to the cache file.
467
     *
468
     * @return void
469
     */
470
    protected function cleanPackageConfiguration()
471
    {
472
        // When we're finished with the internal package configuration, break
473
        // the reference so that it can be garbage-collected:
474
        $this->packageConfig = null;
475
476
        if ($this->config->get('redis-sentinel.clean_config', true) === true) {
477
            $this->config->set('redis-sentinel', [
478
                'Config merged. Set redis-sentinel.clean_config=false to keep.',
479
            ]);
480
        }
481
482
        // Skip loading package config when cached:
483
        $this->config->set('redis-sentinel.load_config', false);
484
    }
485
}
486