Completed
Push — 2.x ( c1417c...43f5a4 )
by Cy
08:56
created

Loader   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 420
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 39
lcom 1
cbo 4
dl 0
loc 420
rs 8.2857
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 1
A load() 0 7 1
A loadConfiguration() 0 12 3
A shouldOverrideLaravelRedisApi() 0 4 1
A shouldLoadConfiguration() 0 8 2
A configureLumenComponents() 0 6 1
A loadPackageConfiguration() 0 14 2
A setConfigurationFor() 0 10 3
A setSessionConfiguration() 0 11 4
A getPackageConfigurationFor() 0 8 2
A mergePackageConfiguration() 0 7 1
B normalizeHosts() 0 18 5
A normalizeConnectionHosts() 0 15 3
A normalizeHost() 0 12 3
A normalizeHostArray() 0 10 2
A normalizeHostString() 0 16 3
A cleanPackageConfiguration() 0 11 2
1
<?php
2
3
namespace Monospice\LaravelRedisSentinel\Configuration;
4
5
use Illuminate\Contracts\Container\Container;
6
use Illuminate\Support\Arr;
7
use Illuminate\Support\Str;
8
9
/**
10
 * The internal configuration loader for the package. Used by the package's
11
 * service provider.
12
 *
13
 * This package provides developers three ways to configure it: through the
14
 * environment, by adding config values to the configuration for the other
15
 * components that the package wraps, and by creating an external package
16
 * configuration file that overrides the default internal configuration.
17
 * The package uses its configuration information to set Redis connection,
18
 * cache, session, and queue configuration values when these are missing.
19
 * This approach simplifies the code needed to configure the package for many
20
 * applications while still providing the flexibility needed for advanced
21
 * setups. This class reconciles each of the configuration methods.
22
 *
23
 * The package's configuration contains partial elements from several other
24
 * component configurations. By default, the package removes its configuration
25
 * after merging the values into each of the appropriate config locations for
26
 * the components it initializes. This behavior prevents the artisan CLI's
27
 * "config:cache" command from saving unnecessary configuration values to the
28
 * configuration cache file. Set the value of "redis-sentinel.clean_config" to
29
 * FALSE to disable this behavior.
30
 *
31
 * To support these configuration scenarios, this class follows these rules:
32
 *
33
 *   - Values in application config files ("config/database.php", etc.) have
34
 *     the greatest precedence. The package will use these values before any
35
 *     others and will not modify these values if they exist.
36
 *   - The package will use values in a developer-supplied package config file
37
 *     located in the application's "config/" directory with the filename of
38
 *     "redis-sentinel.php" for any values not found in the application's
39
 *     standard configuration files before using it's default configuration.
40
 *   - For any configuration values not provided by standard application
41
 *     config files or a developer-supplied custom config file, the package
42
 *     uses it's internal default configuration that reads configuration values
43
 *     from environment variables.
44
 *   - The package will copy values from it's configuration to the standard
45
 *     application configuration at runtime if these are missing. For example,
46
 *     if the application configuration doesn't contain a key for "database.
47
 *     redis-sentinel" (the Redis Sentinel connections), this class will copy
48
 *     its values from "redis-sentinel.database.redis-sentinel" to "database.
49
 *     redis-sentinel".
50
 *   - After loading its configuration, the package must only use configuration
51
 *     values from the standard application config locations. For example, the
52
 *     package will read the values from "database.redis-sentinel" to configure
53
 *     Redis Sentinel connections, not "redis-sentinel.database.redis-sentinel".
54
 *
55
 * @category Package
56
 * @package  Monospice\LaravelRedisSentinel
57
 * @author   Cy Rossignol <[email protected]>
58
 * @license  See LICENSE file
59
 * @link     https://github.com/monospice/laravel-redis-sentinel-drivers
60
 */
61
class Loader
62
{
63
    /**
64
     * The path to the package's default configuration file.
65
     *
66
     * @var string
67
     */
68
    const CONFIG_PATH = __DIR__ . '/../../config/redis-sentinel.php';
69
70
    /**
71
     * Indicates whether the current application runs the Lumen framework.
72
     *
73
     * @var bool
74
     */
75
    public $isLumen;
76
77
    /**
78
     * Indicates whether the current application supports sessions.
79
     *
80
     * @var bool
81
     */
82
    public $supportsSessions;
83
84
    /**
85
     * The current application instance that provides context and services
86
     * used to load the appropriate configuration.
87
     *
88
     * @var Container
89
     */
90
    private $app;
91
92
    /**
93
     * Used to fetch and set application configuration values.
94
     *
95
     * @var \Illuminate\Contracts\Config\Repository
96
     */
97
    private $config;
98
99
    /**
100
     * Contains the set of configuration values used to configure the package
101
     * as loaded from "config/redis-sentinel.php". Empty when the application's
102
     * standard config files provide all the values needed to configure the
103
     * package (such as when a developer provides a custom config).
104
     *
105
     * @var array
106
     */
107
    private $packageConfig;
108
109
    /**
110
     * Initialize the configuration loader. Any actual loading occurs when
111
     * calling the 'loadConfiguration()' method.
112
     *
113
     * @param Container $app The current application instance that provides
114
     * context and services needed to load the appropriate configuration.
115
     */
116
    public function __construct(Container $app)
117
    {
118
        $this->app = $app;
119
        $this->config = $app->make('config');
120
121
        $lumenApplicationClass = 'Laravel\Lumen\Application';
122
123
        $this->isLumen = $app instanceof $lumenApplicationClass;
124
        $this->supportsSessions = $app->bound('session');
125
    }
126
127
    /**
128
     * Create an instance of the loader and load the configuration in one step.
129
     *
130
     * @param Container $app The current application instance that provides
131
     * context and services needed to load the appropriate configuration.
132
     *
133
     * @return self An initialized instance of this class
134
     */
135
    public static function load(Container $app)
136
    {
137
        $loader = new self($app);
138
        $loader->loadConfiguration();
139
140
        return $loader;
141
    }
142
143
    /**
144
     * Load the package configuration.
145
     *
146
     * @return void
147
     */
148
    public function loadConfiguration()
149
    {
150
        if (! $this->shouldLoadConfiguration()) {
151
            return;
152
        }
153
154
        if ($this->isLumen) {
155
            $this->configureLumenComponents();
156
        }
157
158
        $this->loadPackageConfiguration();
159
    }
160
161
    /**
162
     * Determine whether the package should override Laravel's standard Redis
163
     * API ("Redis" facade and "redis" service binding).
164
     *
165
     * @return bool TRUE if the package should override Laravel's standard
166
     * Redis API
167
     */
168
    public function shouldOverrideLaravelRedisApi()
169
    {
170
        return $this->config->get('database.redis.driver') === 'sentinel';
171
    }
172
173
    /**
174
     * Determine if the package should automatically configure itself.
175
     *
176
     * Developers may set the value of "redis-sentinel.load_config" to FALSE to
177
     * disable the package's automatic configuration. This class also sets this
178
     * value to FALSE after loading the package configuration to skip the auto-
179
     * configuration when the application cached its configuration values (via
180
     * "artisan config:cache", for example).
181
     *
182
     * @return bool TRUE if the package should load its configuration
183
     */
184
    protected function shouldLoadConfiguration()
185
    {
186
        if ($this->isLumen) {
187
            $this->app->configure('redis-sentinel');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Illuminate\Contracts\Container\Container as the method configure() does only exist in the following implementations of said interface: Laravel\Lumen\Application.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
188
        }
189
190
        return $this->config->get('redis-sentinel.load_config', true) === true;
191
    }
192
193
    /**
194
     * Configure the Lumen components that this package depends on.
195
     *
196
     * Lumen lazily loads many of its components. We must instruct Lumen to
197
     * load the configuration for components that this class configures so
198
     * that the values are accessible and so that the framework does not
199
     * revert the configuration settings that this class changes when one of
200
     * the components initializes later.
201
     *
202
     * @return void
203
     */
204
    protected function configureLumenComponents()
205
    {
206
        $this->app->configure('database');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Illuminate\Contracts\Container\Container as the method configure() does only exist in the following implementations of said interface: Laravel\Lumen\Application.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
207
        $this->app->configure('cache');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Illuminate\Contracts\Container\Container as the method configure() does only exist in the following implementations of said interface: Laravel\Lumen\Application.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
208
        $this->app->configure('queue');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Illuminate\Contracts\Container\Container as the method configure() does only exist in the following implementations of said interface: Laravel\Lumen\Application.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
209
    }
210
211
    /**
212
     * Reconcile the package configuration and use it to set the appropriate
213
     * configuration values for other application components.
214
     *
215
     * @return void
216
     */
217
    protected function loadPackageConfiguration()
218
    {
219
        $this->setConfigurationFor('database.redis-sentinel');
220
        $this->setConfigurationFor('database.redis.driver');
221
        $this->setConfigurationFor('cache.stores.redis-sentinel');
222
        $this->setConfigurationFor('queue.connections.redis-sentinel');
223
        $this->setSessionConfiguration();
224
225
        $this->normalizeHosts();
226
227
        if ($this->packageConfig !== null) {
228
            $this->cleanPackageConfiguration();
229
        }
230
    }
231
232
    /**
233
     * Set the application configuration value for the specified key with the
234
     * value from the package configuration.
235
     *
236
     * @param string $configKey   The key of the config value to set. Should
237
     * correspond to a key in the package's configuration.
238
     * @param bool   $checkExists If TRUE, don't set the value if the key
239
     * already exists in the application configuration.
240
     *
241
     * @return void
242
     */
243
    protected function setConfigurationFor($configKey, $checkExists = true)
244
    {
245
        if ($checkExists && $this->config->has($configKey)) {
246
            return;
247
        }
248
249
        $config = $this->getPackageConfigurationFor($configKey);
250
251
        $this->config->set($configKey, $config);
252
    }
253
254
    /**
255
     * Set the application session configuration as specified by the package's
256
     * configuration if the app supports sessions.
257
     *
258
     * @return void
259
     */
260
    protected function setSessionConfiguration()
261
    {
262
        if (! $this->supportsSessions
263
            || $this->config->get('session.driver') !== 'redis-sentinel'
264
            || $this->config->get('session.connection') !== null
265
        ) {
266
            return;
267
        }
268
269
        $this->setConfigurationFor('session.connection', false);
270
    }
271
272
    /**
273
     * Get the package configuration for the specified key.
274
     *
275
     * @param string $configKey The key of the configuration value to get
276
     *
277
     * @return mixed The value of the configuration with the specified key
278
     */
279
    protected function getPackageConfigurationFor($configKey)
280
    {
281
        if ($this->packageConfig === null) {
282
            $this->mergePackageConfiguration();
283
        }
284
285
        return Arr::get($this->packageConfig, $configKey);
286
    }
287
288
    /**
289
     * Merge the package's default configuration with the override config file
290
     * supplied by the developer, if any.
291
     *
292
     * @return void
293
     */
294
    protected function mergePackageConfiguration()
295
    {
296
        $defaultConfig = require self::CONFIG_PATH;
297
        $currentConfig = $this->config->get('redis-sentinel', [ ]);
298
299
        $this->packageConfig = array_merge($defaultConfig, $currentConfig);
300
    }
301
302
    /**
303
     * Parse Redis Sentinel connection host definitions to create single host
304
     * entries for host definitions that specify multiple hosts.
305
     *
306
     * To support environment-based configuration using the package's default
307
     * configuration, developers need a way to specifiy multiple Sentinel hosts
308
     * using environment variables. The package supports this requirement by
309
     * allowing developers to provide a comma-delimited string of hosts in a
310
     * "*_HOST" environment variable:
311
     *
312
     *     REDIS_HOST=sentinel1.example.com,sentinel2.example.com
313
     *
314
     * Before parsing the connection configuration, the connection config would
315
     * contain the following value if using the environment variable above:
316
     *
317
     *     'connection' => [
318
     *         [
319
     *             'host' => 'sentinel1.example.com,sentinel2.example.com',
320
     *             'port' => 26379,
321
     *         ]
322
     *     ]
323
     *
324
     * This method will convert the connection configuration to:
325
     *
326
     *     'connection' => [
327
     *         [
328
     *             'host' => 'sentinel1.example.com',
329
     *             'port' => 26379,
330
     *         ],
331
     *         [
332
     *             'host' => 'sentinel2.example.com',
333
     *             'port' => 26379,
334
     *         ]
335
     *     ]
336
     *
337
     * @return void
338
     */
339
    protected function normalizeHosts()
340
    {
341
        $connections = $this->config->get('database.redis-sentinel');
342
343
        if (! is_array($connections)) {
344
            return;
345
        }
346
347
        foreach ($connections as $name => $connection) {
348
            if ($name === 'options' || $name === 'clusters') {
349
                continue;
350
            }
351
352
            $connections[$name] = $this->normalizeConnectionHosts($connection);
353
        }
354
355
        $this->config->set('database.redis-sentinel', $connections);
356
    }
357
358
    /**
359
     * Create single host entries for any host definitions that specify
360
     * multiple hosts in the provided connection configuration.
361
     *
362
     * @param array $connection The connection config which contains the host
363
     * definitions for a single Redis Sentinel connection
364
     *
365
     * @return array The normalized connection configuration values
366
     */
367
    protected function normalizeConnectionHosts(array $connection)
368
    {
369
        $normalized = [ ];
370
371
        if (array_key_exists('options', $connection)) {
372
            $normalized['options'] = $connection['options'];
373
            unset($connection['options']);
374
        }
375
376
        foreach ($connection as $key => $host) {
377
            $normalized = array_merge($normalized, $this->normalizeHost($host));
378
        }
379
380
        return $normalized;
381
    }
382
383
    /**
384
     * Parse the provided host definition into multiple host definitions if it
385
     * specifies more than one host.
386
     *
387
     * @param array|string $host The host definition from a Redis Sentinel
388
     * connection
389
     *
390
     * @return array One or more host definitions parsed from the provided
391
     * host definition
392
     */
393
    protected function normalizeHost($host)
394
    {
395
        if (is_array($host)) {
396
            return $this->normalizeHostArray($host);
397
        }
398
399
        if (is_string($host)) {
400
            return $this->normalizeHostString($host);
401
        }
402
403
        return [ $host ];
404
    }
405
406
    /**
407
     * Parse a host definition in the form of an array into multiple host
408
     * definitions if it specifies more than one host.
409
     *
410
     * @param array $hostArray The host definition from a Redis Sentinel
411
     * connection
412
     *
413
     * @return array One or more host definitions parsed from the provided
414
     * host definition
415
     */
416
    protected function normalizeHostArray(array $hostArray)
417
    {
418
        if (! array_key_exists('host', $hostArray)) {
419
            return [ $hostArray ];
420
        }
421
422
        $port = Arr::get($hostArray, 'port', 26379);
423
424
        return $this->normalizeHostString($hostArray['host'], $port);
425
    }
426
427
    /**
428
     * Parse a host definition in the form of a string into multiple host
429
     * definitions it it specifies more than one host.
430
     *
431
     * @param string $hostString The host definition from a Redis Sentinel
432
     * connection
433
     * @param int    $port       The port number to use for the resulting host
434
     * definitions if the parsed host definition doesn't contain port numbers
435
     *
436
     * @return array One or more host definitions parsed from the provided
437
     * host definition
438
     */
439
    protected function normalizeHostString($hostString, $port = 26379)
440
    {
441
        $hosts = [ ];
442
443
        foreach (explode(',', $hostString) as $host) {
444
            $host = trim($host);
445
446
            if (Str::contains($host, ':')) {
447
                $hosts[] = $host;
448
            } else {
449
                $hosts[] = [ 'host' => $host, 'port' => $port ];
450
            }
451
        }
452
453
        return $hosts;
454
    }
455
456
    /**
457
     * Remove the package's configuration from the application configuration
458
     * repository.
459
     *
460
     * This package's configuration contains partial elements from several
461
     * other component configurations. By default, the package removes its
462
     * configuration after merging the values into each of the appropriate
463
     * config locations for the components it initializes. This behavior
464
     * prevents the artisan "config:cache" command from saving unnecessary
465
     * configuration values to the cache file.
466
     *
467
     * @return void
468
     */
469
    protected function cleanPackageConfiguration()
470
    {
471
        if ($this->config->get('redis-sentinel.clean_config', true) !== true) {
472
            return;
473
        }
474
475
        $this->config->set('redis-sentinel', [
476
            'Config merged. Set "redis-sentinel.clean_config" = false to keep.',
477
            'load_config' => false, // skip loading package config when cached
478
        ]);
479
    }
480
}
481