Passed
Pull Request — master (#42)
by Rustam
03:25
created

Locale   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 288
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 125
c 5
b 0
f 0
dl 0
loc 288
ccs 137
cts 137
cp 1
rs 7.92
wmc 51

20 Methods

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

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
use Yiisoft\Yii\Middleware\Exception\InvalidLocalesFormatException;
24
25
final class Locale implements MiddlewareInterface
26
{
27
    private const DEFAULT_LOCALE = 'en';
28
    private const DEFAULT_LOCALE_NAME = '_language';
29
30
    private bool $enableSaveLocale = true;
31
    private bool $enableDetectLocale = false;
32
    private string $defaultLocale = self::DEFAULT_LOCALE;
33
    private string $queryParameterName = self::DEFAULT_LOCALE_NAME;
34
    private string $sessionName = self::DEFAULT_LOCALE_NAME;
35
    private ?DateInterval $cookieDuration;
36
37
    /**
38
     * @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...
39
     * @param string[] $ignoredRequests
40
     */
41 18
    public function __construct(
42
        private TranslatorInterface $translator,
43
        private UrlGeneratorInterface $urlGenerator,
44
        private SessionInterface $session,
45
        private Aliases $aliases,
46
        private LoggerInterface $logger,
47
        private ResponseFactoryInterface $responseFactory,
48
        private array $locales = [],
49
        private array $ignoredRequests = [],
50
        private bool $cookieSecure = false,
51
        private string $baseUrlAlias = '@baseUrl',
52
    ) {
53 18
        $this->cookieDuration = new DateInterval('P30D');
54
    }
55
56 17
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
57
    {
58 17
        if ($this->locales === []) {
59 1
            return $handler->handle($request);
60
        }
61
62 16
        $this->checkLocales();
63
64 15
        $uri = $request->getUri();
65 15
        $path = $uri->getPath();
66 15
        $query = $uri->getQuery();
67
68 15
        [$locale, $country] = $this->getLocaleFromPath($path);
69
70 15
        if ($locale !== null) {
71 8
            $this->translator->setLocale($locale);
72 8
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
73
74 8
            $response = $handler->handle($request);
75 8
            $newPath = null;
76 8
            if ($this->isDefaultLocale($locale, $country) && $request->getMethod() === Method::GET) {
77 3
                $length = strlen($locale);
78 3
                $newPath = substr($path, $length + 1);
79
            }
80 8
            return $this->applyLocaleFromPath($locale, $response, $query, $newPath);
81
        }
82 7
        if ($this->enableSaveLocale) {
83 7
            [$locale, $country] = $this->getLocaleFromRequest($request);
84
        }
85 7
        if ($locale === null && $this->enableDetectLocale) {
86 2
            [$locale, $country] = $this->detectLocale($request);
87
        }
88 7
        if ($locale === null || $this->isDefaultLocale($locale, $country) || $this->isRequestIgnored($request)) {
89 2
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, null);
90 2
            $request = $request->withUri($uri->withPath('/' . $this->defaultLocale . $path));
91 2
            return $handler->handle($request);
92
        }
93
94 5
        $this->translator->setLocale($locale);
95 5
        $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
96
97 5
        if ($request->getMethod() === Method::GET) {
98 4
            $location = rtrim($this->aliases->get($this->baseUrlAlias), '/') . '/'
99 4
                . $locale . $path . ($query !== '' ? '?' . $query : '');
100 4
            return $this->responseFactory
101 4
                ->createResponse(Status::FOUND)
102 4
                ->withHeader(Header::LOCATION, $location);
103
        }
104
105
106 1
        return $handler->handle($request);
107
    }
108
109 8
    private function applyLocaleFromPath(
110
        string $locale,
111
        ResponseInterface $response,
112
        string $query,
113
        ?string $newPath = null,
114
    ): ResponseInterface {
115 8
        if ($newPath === '') {
116 1
            $newPath = '/';
117
        }
118
119 8
        if ($newPath !== null) {
120 3
            $location = rtrim($this->aliases->get($this->baseUrlAlias), '/')
121 3
                . $newPath . ($query !== '' ? '?' . $query : '');
122 3
            $response = $this->responseFactory
123 3
                ->createResponse(Status::FOUND)
124 3
                ->withHeader(Header::LOCATION, $location);
125
        }
126 8
        if ($this->enableSaveLocale) {
127 8
            $response = $this->saveLocale($locale, $response);
128
        }
129 8
        return $response;
130
    }
131
132
    /**
133
     * @return array{0:string|null, 1:string|null}
134
     */
135 15
    private function getLocaleFromPath(string $path): array
136
    {
137 15
        $parts = [];
138 15
        foreach ($this->locales as $code => $locale) {
139 15
            $parts[] = $code;
140 15
            $parts[] = $locale;
141
        }
142
143 15
        $pattern = implode('|', $parts);
144 15
        if (preg_match("#^/($pattern)\b(/?)#i", $path, $matches)) {
145 8
            $matchedLocale = $matches[1];
146 8
            [$locale, $country] = $this->parseLocale($matchedLocale);
147 8
            if (isset($this->locales[$locale])) {
148 8
                $this->logger->debug(sprintf("Locale '%s' found in URL", $locale));
149 8
                return [$locale, $country];
150
            }
151
        }
152 7
        return [null, null];
153
    }
154
155
    /**
156
     * @return array{0:string|null, 1:string|null}
157
     */
158 7
    private function getLocaleFromRequest(ServerRequestInterface $request): array
159
    {
160
        /** @var array<string, string> $cookies */
161 7
        $cookies = $request->getCookieParams();
162 7
        if (isset($cookies[$this->sessionName])) {
163 1
            $this->logger->debug(sprintf("Locale '%s' found in cookies", $cookies[$this->sessionName]));
164 1
            return $this->parseLocale($cookies[$this->sessionName]);
165
        }
166
        /** @var array<string, string> $queryParameters */
167 6
        $queryParameters = $request->getQueryParams();
168 6
        if (isset($queryParameters[$this->queryParameterName])) {
169 4
            $this->logger->debug(
170 4
                sprintf("Locale '%s' found in query string", $queryParameters[$this->queryParameterName])
171 4
            );
172 4
            return $this->parseLocale($queryParameters[$this->queryParameterName]);
173
        }
174 2
        return [null, null];
175
    }
176
177 14
    private function isDefaultLocale(string $locale, ?string $country): bool
0 ignored issues
show
Unused Code introduced by
The parameter $country is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

177
    private function isDefaultLocale(string $locale, /** @scrutinizer ignore-unused */ ?string $country): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
178
    {
179 14
        return $locale === $this->defaultLocale || $this->locales[$locale] === $this->defaultLocale;
180
    }
181
182
    /**
183
     * @return array{0:string|null, 1:string|null}
184
     */
185 2
    private function detectLocale(ServerRequestInterface $request): array
186
    {
187 2
        foreach ($request->getHeader(Header::ACCEPT_LANGUAGE) as $language) {
188 1
            return $this->parseLocale($language);
189
        }
190 1
        return [null, null];
191
    }
192
193 8
    private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
194
    {
195 8
        $this->logger->debug('Saving found locale to cookies');
196 8
        $this->session->set($this->sessionName, $locale);
197 8
        $cookie = new Cookie(name: $this->sessionName, value: $locale, secure: $this->cookieSecure);
198 8
        if ($this->cookieDuration !== null) {
199 8
            $cookie = $cookie->withMaxAge($this->cookieDuration);
200
        }
201 8
        return $cookie->addToResponse($response);
202
    }
203
204
    /**
205
     * @return array{0:string, 1?:string|null}
206
     */
207 14
    private function parseLocale(string $locale): array
208
    {
209 14
        if (str_contains($locale, '-')) {
210 1
            return explode('-', $locale, 2);
211
        }
212
213 13
        if (str_contains($locale, '_')) {
214 1
            return explode('_', $locale, 2);
215
        }
216
217 12
        return [$locale, null];
218
    }
219
220 6
    private function isRequestIgnored(ServerRequestInterface $request): bool
221
    {
222 6
        foreach ($this->ignoredRequests as $ignoredRequest) {
223 1
            if ((new WildcardPattern($ignoredRequest))->match($request->getUri()->getPath())) {
224 1
                return true;
225
            }
226
        }
227 5
        return false;
228
    }
229
230
    /**
231
     * @throws InvalidLocalesFormatException
232
     */
233 16
    private function checkLocales(): void
234
    {
235 16
        foreach ($this->locales as $code => $locale) {
236 16
            if (!is_string($code) || !is_string($locale)) {
237 1
                throw new InvalidLocalesFormatException();
238
            }
239
        }
240
    }
241
242
    /**
243
     * @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...
244
     *
245
     * @return $this
246
     */
247 1
    public function withLocales(array $locales): self
248
    {
249 1
        $new = clone $this;
250 1
        $new->locales = $locales;
251 1
        return $new;
252
    }
253
254 3
    public function withDefaultLocale(string $defaultLocale): self
255
    {
256 3
        $new = clone $this;
257 3
        $new->defaultLocale = $defaultLocale;
258 3
        return $new;
259
    }
260
261 1
    public function withQueryParameterName(string $queryParameterName): self
262
    {
263 1
        $new = clone $this;
264 1
        $new->queryParameterName = $queryParameterName;
265 1
        return $new;
266
    }
267
268 1
    public function withSessionName(string $sessionName): self
269
    {
270 1
        $new = clone $this;
271 1
        $new->sessionName = $sessionName;
272 1
        return $new;
273
    }
274
275 1
    public function withEnableSaveLocale(bool $enableSaveLocale): self
276
    {
277 1
        $new = clone $this;
278 1
        $new->enableSaveLocale = $enableSaveLocale;
279 1
        return $new;
280
    }
281
282 3
    public function withEnableDetectLocale(bool $enableDetectLocale): self
283
    {
284 3
        $new = clone $this;
285 3
        $new->enableDetectLocale = $enableDetectLocale;
286 3
        return $new;
287
    }
288
289
    /**
290
     * @param string[] $ignoredRequests
291
     *
292
     * @return $this
293
     */
294 2
    public function withIgnoredRequests(array $ignoredRequests): self
295
    {
296 2
        $new = clone $this;
297 2
        $new->ignoredRequests = $ignoredRequests;
298 2
        return $new;
299
    }
300
301 1
    public function withCookieSecure(bool $secure): self
302
    {
303 1
        $new = clone $this;
304 1
        $new->cookieSecure = $secure;
305 1
        return $new;
306
    }
307
308 1
    public function withBaseUrlAlias(string $baseUrlAlias): self
309
    {
310 1
        $new = clone $this;
311 1
        $new->baseUrlAlias = $baseUrlAlias;
312 1
        return $new;
313
    }
314
}
315