Completed
Push — master ( b4d6a5...352f6d )
by Basil
02:42
created

Composition   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 460
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 7

Importance

Changes 0
Metric Value
wmc 44
lcom 2
cbo 7
dl 0
loc 460
rs 8.8798
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A isHostAllowed() 0 14 3
A resolveHostInfo() 0 14 4
A getKeys() 0 8 2
A getPrefixPath() 0 4 1
A getLangShortCode() 0 4 1
A __construct() 0 5 1
A init() 0 17 5
A getResolvedPathInfo() 0 4 1
A setKey() 0 4 1
A getKey() 0 6 2
A createRouteEnsure() 0 9 6
A createRoute() 0 11 3
A prependTo() 0 19 5
A removeFrom() 0 6 1
A getDefaultLangShortCode() 0 4 1
A offsetExists() 0 4 1
A offsetSet() 0 4 1
A offsetGet() 0 4 1
A offsetUnset() 0 4 1
A getLanguage() 0 4 1
A get() 0 4 1
A getFull() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Composition often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Composition, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace luya\web;
4
5
use Yii;
6
use yii\base\Component;
7
use yii\web\ForbiddenHttpException;
8
use luya\Exception;
9
use luya\helpers\StringHelper;
10
use luya\helpers\ArrayHelper;
11
12
/**
13
 * Composition parseRequest Handler.
14
 *
15
 * The composition is run for every {{luya\web\UrlManager::parseRequest()}} call in order to determine
16
 * the language of the application based on {{luya\web\Composition::$hostInfoMapping}} or from the given
17
 * {{luya\web\Request::$hostInfo}}.
18
 *
19
 * It also provides common functions in order to make complex regional language detection for urls
20
 * like `https://example.com/fr/ch`, its possible to set {{luya\web\Composition::$pattern}} and retrieve
21
 * the value later inside your application. The `langShortCode` must be provided as long as working with the
22
 * cms, as its bound to the administration language `admin_lang` database table.
23
 *
24
 * It also provides security check options:
25
 *
26
 * + {{luya\web\Composition::$allowedHosts}}
27
 *
28
 * The Composition component is registered by the {{luya\base\Boot}} object and is therefore accessible
29
 * trough `Yii::$app->composition` as "singleton" instance.
30
 *
31
 * @property string $prefixPath Return the current composition prefix path for the request based on request input and hidden option.
32
 * @property array $keys Return an array with key and value of all resolve composition values for the current request.
33
 * @property string $defaultLangShortCode Return default defined language shord code
34
 * @property string $langShortCode Return wrapper of getKey('langShortCode')
35
 *
36
 * @author Basil Suter <[email protected]>
37
 * @since 1.0.0
38
 */
39
class Composition extends Component implements \ArrayAccess
40
{
41
    const VAR_LANG_SHORT_CODE = 'langShortCode';
42
    
43
    /**
44
     * @var string The Regular-Expression matching the var finder inside the url parts
45
     */
46
    const VAR_MATCH_REGEX = '/<(\w+):?([^>]+)?>/';
47
    
48
    /**
49
     * @deprecated Deprecated in 1.0.5 remove in 1.1.x
50
     */
51
    const EVENT_AFTER_SET = 'EVENT_AFTER_SET';
52
53
    /**
54
     * @var \yii\web\Request Request-Object container from DI
55
     */
56
    public $request;
57
58
    /**
59
     * @var boolean Enable or disable composition prefix out when using `prefixPath`. If disabled the response of prefixPath would be empty, otherwhise it
60
     * returns the full prefix composition pattern based url.
61
     */
62
    public $hidden = true;
63
64
    /**
65
     * @var boolean Disable composition prefixes in URLs only for default language. Takes effect only when `hidden` option is disabled.
66
     * @since 1.0.10
67
     */
68
    public $hideDefaultPrefixOnly = false;
69
70
    /**
71
     * @var string Url matching prefix, which is used for all the modules (e.g. an e-store requireds a language
72
     * as the cms needs this informations too). After proccessing this informations, they will be removed
73
     * from the url for further proccessing.
74
     *
75
     * Examples of how to use patterns:
76
     *
77
     * ```php
78
     * 'pattern' => '<langShortCode:[a-z]{2}>/<countryShortCode:[a-z]{2}>', // de/ch; fr/ch
79
     * ```
80
     */
81
    public $pattern = "<".self::VAR_LANG_SHORT_CODE.":[a-z]{2}>";
82
83
    /**
84
     * @var array Default value if there is no composition provided in the url. The default value must match the url.
85
     */
86
    public $default = [self::VAR_LANG_SHORT_CODE => 'en'];
87
88
    /**
89
     * @var array Define the default behavior for differnet host info schemas, if the host info is not found
90
     * the default behvaior via `$default` will be used.
91
     * An array where the key is the host info and value the array with the default configuration .e.g.
92
     *
93
     * ```php
94
     * 'hostInfoMapping' => [
95
     *     'http://mydomain.com' => ['langShortCode' => 'en'],
96
     *     'http://meinedomain.de' => ['langShortCode' => 'de'],
97
     * ],
98
     * ```
99
     *
100
     * The above configuration must be defined in your compostion componeont configuration in your config file.
101
     */
102
    public $hostInfoMapping = [];
103
    
104
    /**
105
     *
106
     * @var array|string An array with all valid hosts in order to ensure the request host is equals to valid hosts.
107
     * This filter provides protection against ['host header' attacks](https://www.acunetix.com/vulnerabilities/web/host-header-attack).
108
     *
109
     * The allowed hosts check does not care about the protocol (https/http), there fore take a look the {{luya\traits\ApplicationTrait::$ensureSecureConnection}}.
110
     *
111
     * ```php
112
     * 'allowedHosts' => [
113
     *     'example.com', // this does not include www.
114
     *     '*.example.com', // this incluides www. and all other subdomains.
115
     * ]
116
     * ```
117
     *
118
     * > In order to allow all subdomains including www or not use `*example.com`.
119
     *
120
     * If no value is defined, the allowed host filtering is disable, this is the default behavior.
121
     * @since 1.0.5
122
     */
123
    public $allowedHosts;
124
    
125
    /**
126
     * A list of values which are valid for every pattern. If set and a value is provided which is not inside this property
127
     * an http not found exception is thrown.
128
     *
129
     * Every value must be set for the given pattern name:
130
     *
131
     * ```php
132
     * 'expectedValues' => [
133
     *     'langShortCode' => ['en', 'de'], // langShortCode pattern is required
134
     *     'countryShortCode' => ['ch', 'fr', 'de', 'uk'], // additional patterns if configured
135
     * ],
136
     * ```
137
     *
138
     * > This configuration is usual only used in MVC applications without CMS module, as the cms module throws an
139
     * > exception if the requested language is not available.
140
     *
141
     * @var array An array where key is the pattern and value an array of possible values for this pattern.
142
     * @since 1.0.15
143
     */
144
    public $expectedValues = [];
145
146
    /**
147
     * Class constructor, to get data from DiContainer.
148
     *
149
     * @param \luya\web\Request $request Request componet resolved from Depency Manager
150
     * @param array $config The object configuration array
151
     */
152
    public function __construct(Request $request, array $config = [])
153
    {
154
        $this->request = $request;
155
        parent::__construct($config);
156
    }
157
158
    /**
159
     * @inheritdoc
160
     */
161
    public function init()
162
    {
163
        parent::init();
164
165
        // check if the required key langShortCode exist in the default array.
166
        if (!array_key_exists(self::VAR_LANG_SHORT_CODE, $this->default)) {
167
            throw new Exception("The composition default rule must contain a langShortCode.");
168
        }
169
170
        if ($this->allowedHosts !== null && !$this->isHostAllowed($this->allowedHosts)) {
171
            throw new ForbiddenHttpException("Invalid host name.");
172
        }
173
        
174
        if (array_key_exists($this->request->hostInfo, $this->hostInfoMapping)) {
175
            $this->default = $this->hostInfoMapping[$this->request->hostInfo];
176
        }
177
    }
178
    
179
    /**
180
     * Checks if the current request name against the allowedHosts list.
181
     *
182
     * @return boolean Whether the current hostName is allowed or not.
183
     * @since 1.0.5
184
     */
185
    public function isHostAllowed($allowedHosts)
186
    {
187
        $currentHost = $this->request->hostName;
188
        
189
        $rules = (array) $allowedHosts;
190
        
191
        foreach ($rules as $allowedHost) {
192
            if (StringHelper::matchWildcard($allowedHost, $currentHost)) {
193
                return true;
194
            }
195
        }
196
        
197
        return false;
198
    }
199
200
    /**
201
     * Find the host for a given definition based on the {{Composition::$hostInfoMapping}} definition.
202
     *
203
     * Find the host info mapping (if existing) for a lang short code:
204
     *
205
     * ```php
206
     * $host = $composition->resolveHostInfo('en');
207
     * ```
208
     *
209
     * Or resolve by provide full host info mapping defintion:
210
     *
211
     * ```php
212
     * $host = $composition->resolveHostInfo([
213
     *     'langShortCode' => 'de'
214
     *     'countryShortCode' => 'ch',
215
     * ]);
216
     * ```
217
     *
218
     * > Keep in mind that when {{Composition::$hostInfoMapping}} is empty (no defintion), false is returned.
219
     *
220
     * @param string|array $defintion The hostinfo mapping config containing an array with full defintion of different keys or a string
221
     * which will only resolved based on langShortCode identifier.
222
     * @return string|boolean Returns the host name from the host info maping otherwise false if not found.
223
     * @since 1.0.18
224
     */
225
    public function resolveHostInfo($defintion)
226
    {
227
        // if its a scalar value, we assume the user wants to find host info based on languageShortCode
228
        if (is_scalar($defintion)) {
229
            $defintion = [self::VAR_LANG_SHORT_CODE => $defintion];
230
        }
231
232
        $results = $this->hostInfoMapping;
233
        foreach ($defintion as $key => $value) {
234
            $results = ArrayHelper::searchColumns($results, $key, $value);
235
        }
236
237
        return empty($results) ? false : key($results);
238
    }
239
240
    private $_keys;
241
    
242
    /**
243
     * Resolves the current key and value objects based on the current pathInto and pattern from Request component.
244
     *
245
     * @return array An array with key values like `['langShortCode' => 'en']`.
246
     * @since 1.0.5
247
     */
248
    public function getKeys()
249
    {
250
        if ($this->_keys === null) {
251
            $this->_keys = $this->getResolvedPathInfo($this->request)->resolvedValues;
0 ignored issues
show
Compatibility introduced by
$this->request of type object<yii\web\Request> is not a sub-type of object<luya\web\Request>. It seems like you assume a child class of the class yii\web\Request to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
252
        }
253
        
254
        return $this->_keys;
255
    }
256
    
257
    /**
258
     * Resolve the current url request and retun an array contain resolved route and the resolved values.
259
     *
260
     * @param Request $request
261
     * @return \luya\web\CompositionResolver
262
     */
263
    public function getResolvedPathInfo(Request $request)
264
    {
265
        return new CompositionResolver($request, $this);
266
    }
267
268
    /**
269
     * Set a new composition key and value in composition array. If the key already exists, it will
270
     * be overwritten.
271
     *
272
     * @param string $key The key in the array, e.g. langShortCode
273
     * @param string $value The value coresponding to the key e.g. de
274
     */
275
    public function setKey($key, $value)
276
    {
277
        $this->_keys[$key] = $value;
278
    }
279
280
    /**
281
     * Get value from the composition array for the provided key, if the key does not existing the default value
282
     * will be return. The standard value of the defaultValue is false, so if nothing defined and the could not
283
     * be found, the return value is `false`.
284
     *
285
     * @param string $key The key to find in the composition array e.g. langShortCode
286
     * @param string $defaultValue The default value if they could not be found
287
     * @return string|bool
288
     */
289
    public function getKey($key, $defaultValue = false)
290
    {
291
        $this->getKeys();
292
        
293
        return isset($this->_keys[$key]) ? $this->_keys[$key] : $defaultValue;
294
    }
295
    
296
    /**
297
     * Get the composition prefix path based on current provided request.
298
     *
299
     * An example response could be `de` or with other composition keys and patterns `de/ch` or `de-CH`.
300
     *
301
     * @return string A prefix path like `de/ch`
302
     * @since 1.0.5
303
     */
304
    public function getPrefixPath()
305
    {
306
        return $this->createRouteEnsure();
307
    }
308
309
    /**
310
     * Create a route but ensures if composition is hidden anyhow.
311
     *
312
     * @param array $overrideKeys
313
     * @return string
314
     */
315
    public function createRouteEnsure(array $overrideKeys = [])
316
    {
317
        if (isset($overrideKeys[self::VAR_LANG_SHORT_CODE])) {
318
            $langShortCode = $overrideKeys[self::VAR_LANG_SHORT_CODE];
319
        } else {
320
            $langShortCode = $this->langShortCode;
321
        }
322
        return $this->hidden || (!$this->hidden && $langShortCode == $this->defaultLangShortCode && $this->hideDefaultPrefixOnly) ? '' : $this->createRoute($overrideKeys);
323
    }
324
325
    /**
326
     * Create compositon route based on the provided keys (to override), if no keys provided
327
     * all the default values will be used.
328
     *
329
     * @param array $overrideKeys
330
     * @return string
331
     */
332
    public function createRoute(array $overrideKeys = [])
333
    {
334
        $composition = $this->getKeys();
335
        
336
        foreach ($overrideKeys as $key => $value) {
337
            if (array_key_exists($key, $composition)) {
338
                $composition[$key] = $value;
339
            }
340
        }
341
        return implode('/', $composition);
342
    }
343
344
    /**
345
     * Prepend to current composition (or to provided composition prefix-route) to a given route.
346
     *
347
     * Assuming the current composition returns `en/gb` and the route is `foo/bar` the return
348
     * value would be `en/gb/foo/bar`.
349
     *
350
     * If a trailing slash is provided from a route, this will be returned as well, assuming:
351
     *
352
     * ```php
353
     * echo prependTo('/foobar', 'en/gb'); // ouput: /en/gb/foobar
354
     * echo prependTo('foobar', 'en/gb'); // output: en/gb/foobar
355
     * ```
356
     *
357
     * @param string $route The route where the composition prefix should be prepended.
358
     * @param null|string $prefix Define the value you want to prepend to the route or not.
359
     * @return string
360
     */
361
    public function prependTo($route, $prefix = null)
362
    {
363
        if ($prefix === null) {
364
            $prefix = $this->getPrefixPath();
365
        }
366
367
        if (empty($prefix)) {
368
            return $route;
369
        }
370
371
        $prepend = '';
372
373
        // if it contains a prepend slash, we keep this, as long as the route is also longer then just a slash.
374
        if (substr($route, 0, 1) == '/' && strlen($route) > 1) {
375
            $prepend = '/';
376
        }
377
378
        return $prepend.$prefix.'/'.ltrim($route, '/');
379
    }
380
381
    /**
382
     * Remove the composition full parterns from a given route
383
     *
384
     * @param string $route
385
     * @return string route cleanup from the compositon pattern (without).
386
     */
387
    public function removeFrom($route)
388
    {
389
        $pattern = preg_quote($this->prefixPath.'/', '#');
390
391
        return preg_replace("#$pattern#", '', $route, 1);
392
    }
393
    
394
    /**
395
     * Wrapper for `getKey('langShortCode')` to load language to set php env settings.
396
     *
397
     * @return string|boolean Get the language value from the langShortCode key, false if not set.
398
     */
399
    public function getLangShortCode()
400
    {
401
        return $this->getKey(self::VAR_LANG_SHORT_CODE);
402
    }
403
    
404
    /**
405
     * Return the the default langt short code.
406
     *
407
     * @return string
408
     */
409
    public function getDefaultLangShortCode()
410
    {
411
        return $this->default[self::VAR_LANG_SHORT_CODE];
412
    }
413
414
    /**
415
     * ArrayAccess offset exists.
416
     *
417
     * @see \ArrayAccess::offsetExists()
418
     * @return boolean
419
     */
420
    public function offsetExists($offset)
421
    {
422
        return isset($this->_keys[$offset]);
423
    }
424
425
    /**
426
     * ArrayAccess set value to array.
427
     *
428
     * @see \ArrayAccess::offsetSet()
429
     * @param string $offset The key of the array
430
     * @param mixed $value The value for the offset key.
431
     * @throws \luya\Exception
432
     */
433
    public function offsetSet($offset, $value)
434
    {
435
        $this->setKey($offset, $value);
436
    }
437
438
    /**
439
     * ArrayAccess get the value for a key.
440
     *
441
     * @see \ArrayAccess::offsetGet()
442
     * @param string $offset The key to get from the array.
443
     * @return mixed The value for the offset key from the array.
444
     */
445
    public function offsetGet($offset)
446
    {
447
        return $this->getKey($offset, null);
448
    }
449
450
    /**
451
     * ArrayAccess unset key.
452
     *
453
     * Unsetting data via array access is not allowed.
454
     *
455
     * @see \ArrayAccess::offsetUnset()
456
     * @param string $offset The key to unset from the array.
457
     * @throws \luya\Exception
458
     */
459
    public function offsetUnset($offset)
460
    {
461
        throw new Exception('Deleting keys in Composition is not allowed.');
462
    }
463
    
464
    // Deprecated methods
465
    
466
    /**
467
     * Wrapper for `getKey('langShortCode')` to load language to set php env settings.
468
     *
469
     * @return string|boolean Get the language value from the langShortCode key, false if not set.
470
     * @deprecated in 1.1.0 use `getLangShortCode()` instead.
471
     */
472
    public function getLanguage()
473
    {
474
        return $this->getKey(self::VAR_LANG_SHORT_CODE);
475
    }
476
    
477
    /**
478
     * Return the whole composition array.
479
     *
480
     * @return array
481
     * @deprecated Remove in 1.1.0 use `getKeys()` instead.
482
     */
483
    public function get()
484
    {
485
        return $this->_keys;
486
    }
487
    
488
    /**
489
     * Return a path like string with all composition with trailing slash e.g. us/e.
490
     *
491
     * @return string
492
     * @deprecated Remove in 1.1.0 use `getPrefixPath()` instead.
493
     */
494
    public function getFull()
495
    {
496
        return $this->createRouteEnsure();
497
    }
498
}
499