Passed
Pull Request — master (#82)
by
unknown
02:33
created

Locale::__construct()   A

Complexity

Conditions 1
Paths 1

Size

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