Completed
Push — integration-tests ( aca6ec )
by Cy
01:46
created

Loader::mergePackageConfiguration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 7
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
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
12
/**
13
 * The internal configuration loader for the package. Used by the package's
14
 * service provider.
15
 *
16
 * This package provides developers three ways to configure it: through the
17
 * environment, by adding config values to the configuration for the other
18
 * components that the package wraps, and by creating an external package
19
 * configuration file that overrides the default internal configuration.
20
 * The package uses its configuration information to set Redis connection,
21
 * cache, session, and queue configuration values when these are missing.
22
 * This approach simplifies the code needed to configure the package for many
23
 * applications while still providing the flexibility needed for advanced
24
 * setups. This class reconciles each of the configuration methods.
25
 *
26
 * The package's configuration contains partial elements from several other
27
 * component configurations. By default, the package removes its configuration
28
 * after merging the values into each of the appropriate config locations for
29
 * the components it initializes. This behavior prevents the artisan CLI's
30
 * "config:cache" command from saving unnecessary configuration values to the
31
 * configuration cache file. Set the value of "redis-sentinel.clean_config" to
32
 * FALSE to disable this behavior.
33
 *
34
 * To support these configuration scenarios, this class follows these rules:
35
 *
36
 *   - Values in application config files ("config/database.php", etc.) have
37
 *     the greatest precedence. The package will use these values before any
38
 *     others and will not modify these values if they exist.
39
 *   - The package will use values in a developer-supplied package config file
40
 *     located in the application's "config/" directory with the filename of
41
 *     "redis-sentinel.php" for any values not found in the application's
42
 *     standard configuration files before using it's default configuration.
43
 *   - For any configuration values not provided by standard application
44
 *     config files or a developer-supplied custom config file, the package
45
 *     uses it's internal default configuration that reads configuration values
46
 *     from environment variables.
47
 *   - The package will copy values from it's configuration to the standard
48
 *     application configuration at runtime if these are missing. For example,
49
 *     if the application configuration doesn't contain a key for "database.
50
 *     redis-sentinel" (the Redis Sentinel connections), this class will copy
51
 *     its values from "redis-sentinel.database.redis-sentinel" to "database.
52
 *     redis-sentinel".
53
 *   - After loading its configuration, the package must only use configuration
54
 *     values from the standard application config locations. For example, the
55
 *     package will read the values from "database.redis-sentinel" to configure
56
 *     Redis Sentinel connections, not "redis-sentinel.database.redis-sentinel".
57
 *
58
 * @category Package
59
 * @package  Monospice\LaravelRedisSentinel
60
 * @author   Cy Rossignol <[email protected]>
61
 * @license  See LICENSE file
62
 * @link     https://github.com/monospice/laravel-redis-sentinel-drivers
63
 */
64
class Loader
65
{
66
    /**
67
     * The path to the package's default configuration file.
68
     *
69
     * @var string
70
     */
71
    const CONFIG_PATH = __DIR__ . '/../../config/redis-sentinel.php';
72
73
    /**
74
     * Indicates whether the current application runs the Lumen framework.
75
     *
76
     * @var bool
77
     */
78
    public $isLumen;
79
80
    /**
81
     * Indicates whether the current application supports sessions.
82
     *
83
     * @var bool
84
     */
85
    public $supportsSessions;
86
87
    /**
88
     * Indicates whether the package should override Laravel's standard Redis
89
     * API ("Redis" facade and "redis" service binding).
90
     *
91
     * @var bool
92
     */
93
    public $shouldOverrideLaravelRedisApi;
94
95
    /**
96
     * Indicates whether Laravel Horizon is installed.
97
     *
98
     * @var bool
99
     */
100
    public $horizonAvailable;
101
102
    /**
103
     * The current Laravel or Lumen application instance that provides context
104
     * and services used to load the appropriate configuration.
105
     *
106
     * @var LaravelApplication|LumenApplication
107
     */
108
    private $app;
109
110
    /**
111
     * Used to fetch and set application configuration values.
112
     *
113
     * @var \Illuminate\Contracts\Config\Repository
114
     */
115
    private $config;
116
117
    /**
118
     * Contains the set of configuration values used to configure the package
119
     * as loaded from "config/redis-sentinel.php". Empty when the application's
120
     * standard config files provide all the values needed to configure the
121
     * package (such as when a developer provides a custom config).
122
     *
123
     * @var array
124
     */
125
    private $packageConfig;
126
127
    /**
128
     * Initialize the configuration loader. Any actual loading occurs when
129
     * calling the 'loadConfiguration()' method.
130
     *
131
     * @param LaravelApplication|LumenApplication $app The current application
132
     * instance that provides context and services needed to load the
133
     * appropriate configuration.
134
     */
135
    public function __construct($app)
136
    {
137
        $this->app = $app;
138
        $this->config = $app->make('config');
139
140
        $lumenApplicationClass = 'Laravel\Lumen\Application';
141
142
        $this->isLumen = $app instanceof $lumenApplicationClass;
143
        $this->supportsSessions = $app->bound('session');
144
        $this->horizonAvailable = class_exists(Horizon::class);
145
    }
146
147
    /**
148
     * Create an instance of the loader and load the configuration in one step.
149
     *
150
     * @param LaravelApplication|LumenApplication $app The current application
151
     * instance that provides context and services needed to load the
152
     * appropriate configuration.
153
     *
154
     * @return self An initialized instance of this class
155
     */
156
    public static function load($app)
157
    {
158
        $loader = new self($app);
159
        $loader->loadConfiguration();
160
161
        return $loader;
162
    }
163
164
    /**
165
     * Load the package configuration.
166
     *
167
     * @return void
168
     */
169
    public function loadConfiguration()
170
    {
171
        if ($this->shouldLoadConfiguration()) {
172
            if ($this->isLumen) {
173
                $this->configureLumenComponents();
174
            }
175
176
            $this->loadPackageConfiguration();
177
        }
178
179
        // Previous versions of the package looked for the value 'sentinel':
180
        $redisDriver = $this->config->get('database.redis.driver');
181
        $this->shouldOverrideLaravelRedisApi = $redisDriver === 'redis-sentinel'
182
            || $redisDriver === 'sentinel';
183
    }
184
185
    /**
186
     * Get the fully-qualified class name of the RedisSentinelManager class
187
     * for the current version of Laravel or Lumen.
188
     *
189
     * @return string The class name of the appropriate RedisSentinelManager
190
     * with its namespace
191
     */
192
    public function getVersionedRedisSentinelManagerClass()
193
    {
194
        if ($this->isLumen) {
195
            $appVersion = substr($this->app->version(), 7, 3); // ex. "5.4"
196
            $frameworkVersion = '5.4';
197
        } else {
198
            $appVersion = \Illuminate\Foundation\Application::VERSION;
199
            $frameworkVersion = '5.4.20';
200
        }
201
202
        if (version_compare($appVersion, $frameworkVersion, 'lt')) {
203
            return Manager\Laravel540RedisSentinelManager::class;
204
        }
205
206
        return Manager\Laravel5420RedisSentinelManager::class;
207
    }
208
209
    /**
210
     * Fetch the specified application configuration value.
211
     *
212
     * This helper method enables the package's service providers to get config
213
     * values without having to resolve the config service from the container.
214
     *
215
     * @param string|array $key     The key(s) for the value(s) to fetch.
216
     * @param mixed        $default Returned if the key does not exist.
217
     *
218
     * @return mixed The requested configuration value or the provided default
219
     * if the key does not exist.
220
     */
221
    public function get($key, $default = null)
222
    {
223
        return $this->config->get($key, $default);
224
    }
225
226
    /**
227
     * Set the specified application configuration value.
228
     *
229
     * This helper method enables the package's service providers to set config
230
     * values without having to resolve the config service from the container.
231
     *
232
     * @param string|array $key   The key of the value or a tree of values as
233
     * an associative array.
234
     * @param mixed        $value The value to set for the specified key.
235
     *
236
     * @return void
237
     */
238
    public function set($key, $value = null)
239
    {
240
        $this->config->set($key, $value);
241
    }
242
243
    /**
244
     * Determine if the package should automatically configure itself.
245
     *
246
     * Developers may set the value of "redis-sentinel.load_config" to FALSE to
247
     * disable the package's automatic configuration. This class also sets this
248
     * value to FALSE after loading the package configuration to skip the auto-
249
     * configuration when the application cached its configuration values (via
250
     * "artisan config:cache", for example).
251
     *
252
     * @return bool TRUE if the package should load its configuration
253
     */
254
    protected function shouldLoadConfiguration()
255
    {
256
        if ($this->isLumen) {
257
            $this->app->configure('redis-sentinel');
258
        }
259
260
        return $this->config->get('redis-sentinel.load_config', true) === true;
261
    }
262
263
    /**
264
     * Configure the Lumen components that this package depends on.
265
     *
266
     * Lumen lazily loads many of its components. We must instruct Lumen to
267
     * load the configuration for components that this class configures so
268
     * that the values are accessible and so that the framework does not
269
     * revert the configuration settings that this class changes when one of
270
     * the components initializes later.
271
     *
272
     * @return void
273
     */
274
    protected function configureLumenComponents()
275
    {
276
        $this->app->configure('database');
277
        $this->app->configure('broadcasting');
278
        $this->app->configure('cache');
279
        $this->app->configure('queue');
280
    }
281
282
    /**
283
     * Reconcile the package configuration and use it to set the appropriate
284
     * configuration values for other application components.
285
     *
286
     * @return void
287
     */
288
    protected function loadPackageConfiguration()
289
    {
290
        $this->setConfigurationFor('database.redis-sentinel');
291
        $this->setConfigurationFor('database.redis.driver');
292
        $this->setConfigurationFor('broadcasting.connections.redis-sentinel');
293
        $this->setConfigurationFor('cache.stores.redis-sentinel');
294
        $this->setConfigurationFor('queue.connections.redis-sentinel');
295
        $this->setSessionConfiguration();
296
297
        $this->normalizeHosts();
298
299
        if ($this->packageConfig !== null) {
300
            $this->cleanPackageConfiguration();
301
        }
302
    }
303
304
    /**
305
     * Set the application configuration value for the specified key with the
306
     * value from the package configuration.
307
     *
308
     * @param string $configKey   The key of the config value to set. Should
309
     * correspond to a key in the package's configuration.
310
     * @param bool   $checkExists If TRUE, don't set the value if the key
311
     * already exists in the application configuration.
312
     *
313
     * @return void
314
     */
315
    protected function setConfigurationFor($configKey, $checkExists = true)
316
    {
317
        if ($checkExists && $this->config->has($configKey)) {
318
            return;
319
        }
320
321
        $config = $this->getPackageConfigurationFor($configKey);
322
323
        $this->config->set($configKey, $config);
324
    }
325
326
    /**
327
     * Set the application session configuration as specified by the package's
328
     * configuration if the app supports sessions.
329
     *
330
     * @return void
331
     */
332
    protected function setSessionConfiguration()
333
    {
334
        if (! $this->supportsSessions
335
            || $this->config->get('session.driver') !== 'redis-sentinel'
336
            || $this->config->get('session.connection') !== null
337
        ) {
338
            return;
339
        }
340
341
        $this->setConfigurationFor('session.connection', false);
342
    }
343
344
    /**
345
     * Get the package configuration for the specified key.
346
     *
347
     * @param string $configKey The key of the configuration value to get
348
     *
349
     * @return mixed The value of the configuration with the specified key
350
     */
351
    protected function getPackageConfigurationFor($configKey)
352
    {
353
        if ($this->packageConfig === null) {
354
            $this->mergePackageConfiguration();
355
        }
356
357
        return Arr::get($this->packageConfig, $configKey);
358
    }
359
360
    /**
361
     * Merge the package's default configuration with the override config file
362
     * supplied by the developer, if any.
363
     *
364
     * @return void
365
     */
366
    protected function mergePackageConfiguration()
367
    {
368
        $defaultConfig = require self::CONFIG_PATH;
369
        $currentConfig = $this->config->get('redis-sentinel', [ ]);
370
371
        $this->packageConfig = array_merge($defaultConfig, $currentConfig);
372
    }
373
374
    /**
375
     * Parse Redis Sentinel connection host definitions to create single host
376
     * entries for host definitions that specify multiple hosts.
377
     *
378
     * @return void
379
     */
380
    protected function normalizeHosts()
381
    {
382
        $connections = $this->config->get('database.redis-sentinel');
383
384
        if (! is_array($connections)) {
385
            return;
386
        }
387
388
        $this->config->set(
389
            'database.redis-sentinel',
390
            HostNormalizer::normalizeConnections($connections)
391
        );
392
    }
393
394
    /**
395
     * Remove the package's configuration from the application configuration
396
     * repository.
397
     *
398
     * This package's configuration contains partial elements from several
399
     * other component configurations. By default, the package removes its
400
     * configuration after merging the values into each of the appropriate
401
     * config locations for the components it initializes. This behavior
402
     * prevents the artisan "config:cache" command from saving unnecessary
403
     * configuration values to the cache file.
404
     *
405
     * @return void
406
     */
407
    protected function cleanPackageConfiguration()
408
    {
409
        // When we're finished with the internal package configuration, break
410
        // the reference so that it can be garbage-collected:
411
        $this->packageConfig = null;
412
413
        if ($this->config->get('redis-sentinel.clean_config', true) !== true) {
414
            return;
415
        }
416
417
        $this->config->set('redis-sentinel', [
418
            'Config merged. Set "redis-sentinel.clean_config" = false to keep.',
419
            'load_config' => false, // skip loading package config when cached
420
        ]);
421
    }
422
}
423