Completed
Pull Request — 2.x (#19)
by
unknown
10:14
created

Loader::getApplication()   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 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
     * Get the current Laravel or Lumen application.
248
     *
249
     * @return \Illuminate\Contracts\Foundation\Application
0 ignored issues
show
Documentation introduced by
Should the return type not be LaravelApplication|LumenApplication?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

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