Passed
Push — master ( 650988...cc92ec )
by
unknown
02:35
created

Locale::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 8
dl 0
loc 12
ccs 3
cts 3
cp 1
crap 1
rs 10

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Middleware;
6
7
use DateInterval;
8
use InvalidArgumentException;
9
use Psr\EventDispatcher\EventDispatcherInterface;
10
use Psr\Http\Message\ResponseFactoryInterface;
11
use Psr\Http\Message\ResponseInterface;
12
use Psr\Http\Message\ServerRequestInterface;
13
use Psr\Http\Server\MiddlewareInterface;
14
use Psr\Http\Server\RequestHandlerInterface;
15
use Psr\Log\LoggerInterface;
16
use Yiisoft\Cookies\Cookie;
17
use Yiisoft\Http\Header;
18
use Yiisoft\Http\Method;
19
use Yiisoft\Http\Status;
20
use Yiisoft\Router\UrlGeneratorInterface;
21
use Yiisoft\Strings\WildcardPattern;
22
use Yiisoft\Yii\Middleware\Event\SetLocaleEvent;
23
use Yiisoft\Yii\Middleware\Exception\InvalidLocalesFormatException;
24
25
use function array_key_exists;
26
27
/**
28
 * Locale middleware supports locale-based routing and configures URL generator. With {@see SetLocaleEvent} it's also
29
 * possible to configure locale in other services such as translator or session.
30
 *
31
 * You should place it before `Route` middleware in the middleware list.
32
 */
33
final class Locale implements MiddlewareInterface
34
{
35
    private const DEFAULT_LOCALE = 'en';
36
    private const DEFAULT_LOCALE_NAME = '_language';
37
    private const LOCALE_SEPARATORS = ['-', '_'];
38
39
    private bool $detectLocale = false;
40
    private string $defaultLocale = self::DEFAULT_LOCALE;
41
    private string $queryParameterName = self::DEFAULT_LOCALE_NAME;
42
    private string $cookieName = self::DEFAULT_LOCALE_NAME;
43
    /**
44
     * @psalm-var array<string, string>
45
     */
46
    private array $supportedLocales;
47
48
    /**
49
     * @param EventDispatcherInterface $eventDispatcher Event dispatcher instance to dispatch events.
50
     * @param UrlGeneratorInterface $urlGenerator URL generator instance to set locale for.
51
     * @param LoggerInterface $logger Logger instance to write debug logs to.
52
     * @param ResponseFactoryInterface $responseFactory Response factory used to create redirect responses.
53
     * @param array $supportedLocales List of supported locales in key-value format such as `['ru' => 'ru_RU', 'uz' => 'uz_UZ']`.
54
     * @param string[] $ignoredRequestUrlPatterns {@see WildcardPattern Patterns} for ignoring requests with URLs matching.
55
     * @param ?DateInterval $cookieDuration Locale cookie lifetime. `null` disables saving locale to cookies completely.
56
     * @param bool $secureCookie Whether middleware should flag locale cookie as secure. Effective only when
57
     * {@see $cookieDuration} isn't `null`.
58
     */
59 40
    public function __construct(
60
        private EventDispatcherInterface $eventDispatcher,
61
        private UrlGeneratorInterface $urlGenerator,
62
        private LoggerInterface $logger,
63
        private ResponseFactoryInterface $responseFactory,
64
        array $supportedLocales = [],
65
        private array $ignoredRequestUrlPatterns = [],
66
        private bool $secureCookie = false,
67
        private ?DateInterval $cookieDuration = null,
68
    ) {
69 40
        $this->assertSupportedLocalesFormat($supportedLocales);
70 36
        $this->supportedLocales = $supportedLocales;
71
    }
72
73 33
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
74
    {
75 33
        if (empty($this->supportedLocales)) {
76 1
            return $handler->handle($request);
77
        }
78
79 32
        $uri = $request->getUri();
80 32
        $path = $uri->getPath();
81 32
        $query = $uri->getQuery();
82 32
        $newPath = null;
83 32
        $locale = $this->getLocaleFromPath($path);
84
85 32
        if ($locale !== null) {
86 16
            if ($request->getMethod() === Method::GET) {
87 16
                $newPath = substr($path, strlen($locale) + 1) ?: '/';
88
            }
89
        } else {
90
            /** @psalm-var array<string, string> $queryParameters */
91 16
            $queryParameters = $request->getQueryParams();
92 16
            $locale = $this->getLocaleFromQuery($queryParameters);
93
94 16
            if ($locale === null) {
95
                /** @psalm-var array<string, string> $cookieParameters */
96 10
                $cookieParameters = $request->getCookieParams();
97 10
                $locale = $this->getLocaleFromCookies($cookieParameters);
98
            }
99
100 16
            if ($locale === null && $this->detectLocale) {
101 7
                $locale = $this->detectLocale($request);
102
            }
103
104 16
            if ($locale === null || $locale === $this->defaultLocale || $this->isRequestIgnored($request)) {
105 3
                $this->urlGenerator->setDefaultArgument($this->queryParameterName, null);
106 3
                $request = $request->withUri($uri->withPath('/' . $this->defaultLocale . $path));
107
108 3
                return $handler->handle($request);
109
            }
110
111 13
            if ($request->getMethod() === Method::GET) {
112 12
                $newPath = '/' . $locale . $path;
113
            }
114
        }
115
116 29
        $response = $handler->handle($request);
117 29
        if ($newPath !== null) {
118 27
            $location = $this->getBaseUrl() . $newPath . ($query !== '' ? '?' . $query : '');
119 27
            $response = $this
120 27
                ->responseFactory
121 27
                ->createResponse(Status::FOUND)
122 27
                ->withHeader(Header::LOCATION, $location);
123
        }
124
125
        /** @var string $locale */
126 29
        $this->eventDispatcher->dispatch(new SetLocaleEvent($this->supportedLocales[$locale]));
127 29
        $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
128
129 29
        return $this->saveLocale($locale, $response);
130
    }
131
132 32
    private function getLocaleFromPath(string $path): ?string
133
    {
134 32
        $parts = [];
135 32
        foreach ($this->supportedLocales as $code => $locale) {
136 32
            $parts[] = $code;
137 32
            $parts[] = $locale;
138
        }
139
140 32
        $pattern = implode('|', $parts);
141 32
        if (preg_match("#^/($pattern)\b(/?)#i", $path, $matches)) {
142 16
            $matchedLocale = $matches[1];
143 16
            if (!isset($this->supportedLocales[$matchedLocale])) {
144 1
                $matchedLocale = $this->parseLocale($matchedLocale);
145
            }
146 16
            if (isset($this->supportedLocales[$matchedLocale])) {
147 16
                $this->logger->debug(sprintf("Locale '%s' found in URL.", $matchedLocale));
148 16
                return $matchedLocale;
149
            }
150
        }
151 16
        return null;
152
    }
153
154
    /**
155
     * @psalm-param array<string, string> $queryParameters
156
     */
157 16
    private function getLocaleFromQuery($queryParameters): ?string
158
    {
159 16
        if (!isset($queryParameters[$this->queryParameterName])) {
160 10
            return null;
161
        }
162
163 6
        $this->logger->debug(
164 6
            sprintf("Locale '%s' found in query string.", $queryParameters[$this->queryParameterName]),
165 6
        );
166
167 6
        return $this->parseLocale($queryParameters[$this->queryParameterName]);
168
    }
169
170
    /**
171
     * @psalm-param array<string, string> $cookieParameters
172
     */
173 10
    private function getLocaleFromCookies($cookieParameters): ?string
174
    {
175 10
        if (!isset($cookieParameters[$this->cookieName])) {
176 8
            return null;
177
        }
178
179 2
        $this->logger->debug(sprintf("Locale '%s' found in cookies.", $cookieParameters[$this->cookieName]));
180
181 2
        return $this->parseLocale($cookieParameters[$this->cookieName]);
182
    }
183
184 7
    private function detectLocale(ServerRequestInterface $request): ?string
185
    {
186 7
        foreach ($request->getHeader(Header::ACCEPT_LANGUAGE) as $language) {
187 6
            if (!isset($this->supportedLocales[$language])) {
188 5
                $language = $this->parseLocale($language);
189
            }
190 6
            if (isset($this->supportedLocales[$language])) {
191 6
                return $language;
192
            }
193
        }
194 1
        return null;
195
    }
196
197 29
    private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
198
    {
199 29
        if ($this->cookieDuration === null) {
200 25
            return $response;
201
        }
202
203 4
        $this->logger->debug('Saving found locale to cookies.');
204 4
        $cookie = new Cookie(name: $this->cookieName, value: $locale, secure: $this->secureCookie);
205 4
        $cookie = $cookie->withMaxAge($this->cookieDuration);
206
207 4
        return $cookie->addToResponse($response);
208
    }
209
210 14
    private function parseLocale(string $locale): string
211
    {
212 14
        foreach (self::LOCALE_SEPARATORS as $separator) {
213 14
            $separatorPosition = strpos($locale, $separator);
214 14
            if ($separatorPosition !== false) {
215 6
                return substr($locale, 0, $separatorPosition);
216
            }
217
        }
218
219 8
        return $locale;
220
    }
221
222 14
    private function isRequestIgnored(ServerRequestInterface $request): bool
223
    {
224 14
        foreach ($this->ignoredRequestUrlPatterns as $ignoredRequest) {
225 1
            if ((new WildcardPattern($ignoredRequest))->match($request->getUri()->getPath())) {
226 1
                return true;
227
            }
228
        }
229 13
        return false;
230
    }
231
232
    /**
233
     * @psalm-assert array<string, string> $supportedLocales
234
     *
235
     * @throws InvalidLocalesFormatException
236
     */
237 40
    private function assertSupportedLocalesFormat(array $supportedLocales): void
238
    {
239 40
        foreach ($supportedLocales as $code => $locale) {
240 39
            if (!is_string($code) || !is_string($locale)) {
241 4
                throw new InvalidLocalesFormatException();
242
            }
243
        }
244
    }
245
246 27
    private function getBaseUrl(): string
247
    {
248 27
        return rtrim($this->urlGenerator->getUriPrefix(), '/');
249
    }
250
251
    /**
252
     * Return new instance with supported locales specified.
253
     *
254
     * @param array $locales List of supported locales in key-value format such as `['ru' => 'ru_RU', 'uz' => 'uz_UZ']`.
255
     *
256
     * @throws InvalidLocalesFormatException
257
     */
258 1
    public function withSupportedLocales(array $locales): self
259
    {
260 1
        $this->assertSupportedLocalesFormat($locales);
261 1
        $new = clone $this;
262 1
        $new->supportedLocales = $locales;
263 1
        return $new;
264
    }
265
266
    /**
267
     * Return new instance with default locale specified.
268
     *
269
     * @param string $defaultLocale Default locale. It must be present as a key in {@see $supportedLocales}.
270
     */
271 8
    public function withDefaultLocale(string $defaultLocale): self
272
    {
273 8
        if (!array_key_exists($defaultLocale, $this->supportedLocales)) {
274 2
            throw new InvalidArgumentException('Default locale allows only keys from supported locales.');
275
        }
276
277 6
        $new = clone $this;
278 6
        $new->defaultLocale = $defaultLocale;
279 6
        return $new;
280
    }
281
282
    /**
283
     * Return new instance with the name of the query string parameter to look for locale.
284
     *
285
     * @param string $queryParameterName Name of the query string parameter.
286
     */
287 2
    public function withQueryParameterName(string $queryParameterName): self
288
    {
289 2
        $new = clone $this;
290 2
        $new->queryParameterName = $queryParameterName;
291 2
        return $new;
292
    }
293
294
    /**
295
     * Return new instance with the name of cookie parameter to store found locale. Effective only when
296
     * {@see $cookieDuration} isn't `null`.
297
     *
298
     * @param string $cookieName Name of cookie parameter.
299
     */
300 3
    public function withCookieName(string $cookieName): self
301
    {
302 3
        $new = clone $this;
303 3
        $new->cookieName = $cookieName;
304 3
        return $new;
305
    }
306
307
    /**
308
     * Return new instance with enabled or disabled detection of locale based on `Accept-Language` header.
309
     *
310
     * @param bool $enabled Whether middleware should detect locale.
311
     */
312 9
    public function withDetectLocale(bool $enabled): self
313
    {
314 9
        $new = clone $this;
315 9
        $new->detectLocale = $enabled;
316 9
        return $new;
317
    }
318
319
    /**
320
     * Return new instance with {@see WildcardPattern patterns} for ignoring requests with URLs matching.
321
     *
322
     * @param string[] $patterns Patterns.
323
     */
324 2
    public function withIgnoredRequestUrlPatterns(array $patterns): self
325
    {
326 2
        $new = clone $this;
327 2
        $new->ignoredRequestUrlPatterns = $patterns;
328 2
        return $new;
329
    }
330
331
    /**
332
     * Return new instance with enabled or disabled secure cookies.
333
     *
334
     * @param bool $secure Whether middleware should flag locale cookie as secure.
335
     */
336 3
    public function withSecureCookie(bool $secure): self
337
    {
338 3
        $new = clone $this;
339 3
        $new->secureCookie = $secure;
340 3
        return $new;
341
    }
342
343
    /**
344
     * Return new instance with changed cookie duration.
345
     *
346
     * @param ?DateInterval $cookieDuration Locale cookie lifetime. When set to `null`, saving locale to cookies is
347
     * disabled completely.
348
     */
349 5
    public function withCookieDuration(?DateInterval $cookieDuration): self
350
    {
351 5
        $new = clone $this;
352 5
        $new->cookieDuration = $cookieDuration;
353 5
        return $new;
354
    }
355
}
356