Passed
Pull Request — master (#42)
by Rustam
02:54
created

Locale   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 280
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 55
eloc 126
c 3
b 0
f 0
dl 0
loc 280
ccs 136
cts 136
cp 1
rs 6

19 Methods

Rating   Name   Duplication   Size   Complexity  
A withCookieSecure() 0 5 1
A saveLocale() 0 9 2
A __construct() 0 13 1
A getLocaleFromRequest() 0 17 3
A withIgnoredRequests() 0 5 1
A detectLocale() 0 6 2
C process() 0 49 13
A withBaseUrlAlias() 0 5 1
A applyLocaleFromPath() 0 21 5
A getLocaleFromPath() 0 18 6
A isDefaultLocale() 0 3 3
A isRequestIgnored() 0 8 3
A withEnableSaveLocale() 0 5 1
A withQueryParameterName() 0 5 1
A withDefaultLocale() 0 5 1
B parseLocale() 0 17 8
A withSessionName() 0 5 1
A withLocales() 0 5 1
A withEnableDetectLocale() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Locale 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.

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 Locale, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Middleware;
6
7
use DateInterval;
8
use Psr\Http\Message\ResponseFactoryInterface;
9
use Psr\Http\Message\ResponseInterface;
10
use Psr\Http\Message\ServerRequestInterface;
11
use Psr\Http\Server\MiddlewareInterface;
12
use Psr\Http\Server\RequestHandlerInterface;
13
use Psr\Log\LoggerInterface;
14
use Yiisoft\Aliases\Aliases;
15
use Yiisoft\Cookies\Cookie;
16
use Yiisoft\Http\Header;
17
use Yiisoft\Http\Method;
18
use Yiisoft\Http\Status;
19
use Yiisoft\Router\UrlGeneratorInterface;
20
use Yiisoft\Session\SessionInterface;
21
use Yiisoft\Strings\WildcardPattern;
22
use Yiisoft\Translator\TranslatorInterface;
23
24
final class Locale implements MiddlewareInterface
25
{
26
    private const DEFAULT_LOCALE = 'en';
27
    private const DEFAULT_LOCALE_NAME = '_language';
28
29
    private bool $enableSaveLocale = true;
30
    private bool $enableDetectLocale = false;
31
    private string $defaultLocale = self::DEFAULT_LOCALE;
32
    private string $queryParameterName = self::DEFAULT_LOCALE_NAME;
33
    private string $sessionName = self::DEFAULT_LOCALE_NAME;
34
    private ?DateInterval $cookieDuration;
35
36
    /**
37
     * @param array<array-key, string> $locales
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, string> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, string>.
Loading history...
38
     * @param string[] $ignoredRequests
39
     */
40 16
    public function __construct(
41
        private TranslatorInterface $translator,
42
        private UrlGeneratorInterface $urlGenerator,
43
        private SessionInterface $session,
44
        private Aliases $aliases,
45
        private LoggerInterface $logger,
46
        private ResponseFactoryInterface $responseFactory,
47
        private array $locales = [],
48
        private array $ignoredRequests = [],
49
        private bool $cookieSecure = false,
50
        private string $baseUrlAlias = '@baseUrl',
51
    ) {
52 16
        $this->cookieDuration = new DateInterval('P30D');
53
    }
54
55 15
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
56
    {
57 15
        if ($this->locales === []) {
58 1
            return $handler->handle($request);
59
        }
60
61 14
        $uri = $request->getUri();
62 14
        $path = $uri->getPath();
63 14
        $query = $uri->getQuery();
64
65 14
        [$locale, $country] = $this->getLocaleFromPath($path);
66
67 14
        if ($locale !== null) {
68 8
            $this->translator->setLocale($locale);
69 8
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
70
71 8
            $response = $handler->handle($request);
72 8
            $newPath = null;
73 8
            if ($this->isDefaultLocale($locale, $country) && $request->getMethod() === Method::GET) {
74 3
                $length = strlen($locale);
75 3
                $newPath = substr($path, $length + 1);
76
            }
77 8
            return $this->applyLocaleFromPath($locale, $response, $query, $newPath);
78
        }
79 6
        if ($this->enableSaveLocale) {
80 6
            [$locale, $country] = $this->getLocaleFromRequest($request);
81
        }
82 6
        if ($locale === null && $this->enableDetectLocale) {
83 2
            [$locale, $country] = $this->detectLocale($request);
84
        }
85 6
        if ($locale === null || $this->isDefaultLocale($locale, $country) || $this->isRequestIgnored($request)) {
86 2
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, null);
87 2
            $request = $request->withUri($uri->withPath('/' . $this->defaultLocale . $path));
88 2
            return $handler->handle($request);
89
        }
90
91 4
        $this->translator->setLocale($locale);
92 4
        $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
93
94 4
        if ($request->getMethod() === Method::GET) {
95 3
            $location = rtrim($this->aliases->get($this->baseUrlAlias), '/') . '/'
96 3
                . $locale . $path . ($query !== '' ? '?' . $query : '');
97 3
            return $this->responseFactory
98 3
                ->createResponse(Status::FOUND)
99 3
                ->withHeader(Header::LOCATION, $location);
100
        }
101
102
103 1
        return $handler->handle($request);
104
    }
105
106 8
    private function applyLocaleFromPath(
107
        string $locale,
108
        ResponseInterface $response,
109
        string $query,
110
        ?string $newPath = null,
111
    ): ResponseInterface {
112 8
        if ($newPath === '') {
113 1
            $newPath = '/';
114
        }
115
116 8
        if ($newPath !== null) {
117 3
            $location = rtrim($this->aliases->get($this->baseUrlAlias), '/')
118 3
                . $newPath . ($query !== '' ? '?' . $query : '');
119 3
            $response = $this->responseFactory
120 3
                ->createResponse(Status::FOUND)
121 3
                ->withHeader(Header::LOCATION, $location);
122
        }
123 8
        if ($this->enableSaveLocale) {
124 8
            $response = $this->saveLocale($locale, $response);
125
        }
126 8
        return $response;
127
    }
128
129
    /**
130
     * @return list{string|null, string|null}
0 ignored issues
show
Bug introduced by
The type Yiisoft\Yii\Middleware\list was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
131
     */
132 14
    private function getLocaleFromPath(string $path): array
133
    {
134 14
        $parts = [];
135 14
        foreach ($this->locales as $code => $locale) {
136 14
            $lang = is_string($code) ? $code : $locale;
137 14
            $parts[] = $lang;
138
        }
139
140 14
        $pattern = implode('|', $parts);
141 14
        if (preg_match("#^/($pattern)\b(/?)#i", $path, $matches)) {
142 8
            $matchedLocale = $matches[1];
143 8
            [$locale, $country] = $this->parseLocale($matchedLocale);
144 8
            if (isset($this->locales[$locale]) || in_array($matchedLocale, $this->locales, true)) {
145 8
                $this->logger->debug(sprintf("Locale '%s' found in URL", $locale));
146 8
                return [$locale, $country];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array($locale, $country) returns the type array which is incompatible with the documented return type Yiisoft\Yii\Middleware\list.
Loading history...
147
            }
148
        }
149 6
        return [null, null];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array(null, null) returns the type array<integer,null> which is incompatible with the documented return type Yiisoft\Yii\Middleware\list.
Loading history...
150
    }
151
152
    /**
153
     * @return list{string|null, string|null}
154
     */
155 6
    private function getLocaleFromRequest(ServerRequestInterface $request): array
156
    {
157
        /** @var array<string, string> $cookies */
158 6
        $cookies = $request->getCookieParams();
159 6
        if (isset($cookies[$this->sessionName])) {
160 1
            $this->logger->debug(sprintf("Locale '%s' found in cookies", $cookies[$this->sessionName]));
161 1
            return $this->parseLocale($cookies[$this->sessionName]);
162
        }
163
        /** @var array<string, string> $queryParameters */
164 5
        $queryParameters = $request->getQueryParams();
165 5
        if (isset($queryParameters[$this->queryParameterName])) {
166 3
            $this->logger->debug(
167 3
                sprintf("Locale '%s' found in query string", $queryParameters[$this->queryParameterName])
168 3
            );
169 3
            return $this->parseLocale($queryParameters[$this->queryParameterName]);
170
        }
171 2
        return [null, null];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array(null, null) returns the type array<integer,null> which is incompatible with the documented return type Yiisoft\Yii\Middleware\list.
Loading history...
172
    }
173
174 13
    private function isDefaultLocale(string $locale, ?string $country): bool
175
    {
176 13
        return $locale === $this->defaultLocale || ($country !== null && $this->defaultLocale === "$locale-$country");
177
    }
178
179
    /**
180
     * @return list{string|null, string|null}
181
     */
182 2
    private function detectLocale(ServerRequestInterface $request): array
183
    {
184 2
        foreach ($request->getHeader(Header::ACCEPT_LANGUAGE) as $language) {
185 1
            return $this->parseLocale($language);
186
        }
187 1
        return [null, null];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array(null, null) returns the type array<integer,null> which is incompatible with the documented return type Yiisoft\Yii\Middleware\list.
Loading history...
188
    }
189
190 8
    private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
191
    {
192 8
        $this->logger->debug('Saving found locale to cookies');
193 8
        $this->session->set($this->sessionName, $locale);
194 8
        $cookie = new Cookie(name: $this->sessionName, value: $locale, secure: $this->cookieSecure);
195 8
        if ($this->cookieDuration !== null) {
196 8
            $cookie = $cookie->withMaxAge($this->cookieDuration);
197
        }
198 8
        return $cookie->addToResponse($response);
199
    }
200
201
    /**
202
     * @return list{string, string|null}
203
     */
204 13
    private function parseLocale(string $locale): array
205
    {
206 13
        $localeParts = [];
207 13
        if (str_contains($locale, '-') || str_contains($locale, '_')) {
208 2
            $localeParts = preg_split('/[-_]/', $locale, 2);
209
        } elseif (
210 11
            isset($this->locales[$locale]) &&
211 11
            (str_contains($this->locales[$locale], '-') || str_contains($this->locales[$locale], '_'))
212
        ) {
213 10
            $localeParts = preg_split('/[-_]/', $this->locales[$locale], 2);
214
        }
215
216 13
        if (!empty($localeParts) && count($localeParts) === 2) {
217 12
            return $localeParts;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $localeParts returns the type array|string[] which is incompatible with the documented return type Yiisoft\Yii\Middleware\list.
Loading history...
218
        }
219
220 1
        return [$locale, null];
0 ignored issues
show
Bug Best Practice introduced by
The expression return array($locale, null) returns the type array<integer,null|string> which is incompatible with the documented return type Yiisoft\Yii\Middleware\list.
Loading history...
221
    }
222
223 5
    private function isRequestIgnored(ServerRequestInterface $request): bool
224
    {
225 5
        foreach ($this->ignoredRequests as $ignoredRequest) {
226 1
            if ((new WildcardPattern($ignoredRequest))->match($request->getUri()->getPath())) {
227 1
                return true;
228
            }
229
        }
230 4
        return false;
231
    }
232
233
    /**
234
     * @param array<array-key, string> $locales
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<array-key, string> at position 2 could not be parsed: Unknown type name 'array-key' at position 2 in array<array-key, string>.
Loading history...
235
     *
236
     * @return $this
237
     */
238 1
    public function withLocales(array $locales): self
239
    {
240 1
        $new = clone $this;
241 1
        $new->locales = $locales;
242 1
        return $new;
243
    }
244
245 3
    public function withDefaultLocale(string $defaultLocale): self
246
    {
247 3
        $new = clone $this;
248 3
        $new->defaultLocale = $defaultLocale;
249 3
        return $new;
250
    }
251
252 1
    public function withQueryParameterName(string $queryParameterName): self
253
    {
254 1
        $new = clone $this;
255 1
        $new->queryParameterName = $queryParameterName;
256 1
        return $new;
257
    }
258
259 1
    public function withSessionName(string $sessionName): self
260
    {
261 1
        $new = clone $this;
262 1
        $new->sessionName = $sessionName;
263 1
        return $new;
264
    }
265
266 1
    public function withEnableSaveLocale(bool $enableSaveLocale): self
267
    {
268 1
        $new = clone $this;
269 1
        $new->enableSaveLocale = $enableSaveLocale;
270 1
        return $new;
271
    }
272
273 3
    public function withEnableDetectLocale(bool $enableDetectLocale): self
274
    {
275 3
        $new = clone $this;
276 3
        $new->enableDetectLocale = $enableDetectLocale;
277 3
        return $new;
278
    }
279
280
    /**
281
     * @param string[] $ignoredRequests
282
     *
283
     * @return $this
284
     */
285 2
    public function withIgnoredRequests(array $ignoredRequests): self
286
    {
287 2
        $new = clone $this;
288 2
        $new->ignoredRequests = $ignoredRequests;
289 2
        return $new;
290
    }
291
292 1
    public function withCookieSecure(bool $secure): self
293
    {
294 1
        $new = clone $this;
295 1
        $new->cookieSecure = $secure;
296 1
        return $new;
297
    }
298
299 1
    public function withBaseUrlAlias(string $baseUrlAlias): self
300
    {
301 1
        $new = clone $this;
302 1
        $new->baseUrlAlias = $baseUrlAlias;
303 1
        return $new;
304
    }
305
}
306