Passed
Pull Request — master (#82)
by Alexander
04:39 queued 02:12
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
        return $this->saveLocale($locale, $response);
135
    }
136
137 32
    private function getLocaleFromPath(string $path): ?string
138
    {
139 32
        $parts = [];
140 32
        foreach ($this->supportedLocales as $code => $locale) {
141 32
            $parts[] = $code;
142 32
            $parts[] = $locale;
143
        }
144
145 32
        $pattern = implode('|', $parts);
146 32
        if (preg_match("#^/($pattern)\b(/?)#i", $path, $matches)) {
147 16
            $matchedLocale = $matches[1];
148 16
            if (!isset($this->supportedLocales[$matchedLocale])) {
149 1
                $matchedLocale = $this->parseLocale($matchedLocale);
150
            }
151 16
            if (isset($this->supportedLocales[$matchedLocale])) {
152 16
                $this->logger->debug(sprintf("Locale '%s' found in URL.", $matchedLocale));
153 16
                return $matchedLocale;
154
            }
155
        }
156 16
        return null;
157
    }
158
159
    /**
160
     * @psalm-param array<string, string> $queryParameters
161
     */
162 16
    private function getLocaleFromQuery($queryParameters): ?string
163
    {
164 16
        if (!isset($queryParameters[$this->queryParameterName])) {
165 10
            return null;
166
        }
167
168 6
        $this->logger->debug(
169 6
            sprintf("Locale '%s' found in query string.", $queryParameters[$this->queryParameterName]),
170 6
        );
171
172 6
        return $this->parseLocale($queryParameters[$this->queryParameterName]);
173
    }
174
175
    /**
176
     * @psalm-param array<string, string> $cookieParameters
177
     */
178 10
    private function getLocaleFromCookies($cookieParameters): ?string
179
    {
180 10
        if (!isset($cookieParameters[$this->cookieName])) {
181 8
            return null;
182
        }
183
184 2
        $this->logger->debug(sprintf("Locale '%s' found in cookies.", $cookieParameters[$this->cookieName]));
185
186 2
        return $this->parseLocale($cookieParameters[$this->cookieName]);
187
    }
188
189 7
    private function detectLocale(ServerRequestInterface $request): ?string
190
    {
191 7
        foreach ($request->getHeader(Header::ACCEPT_LANGUAGE) as $language) {
192 6
            if (!isset($this->supportedLocales[$language])) {
193 5
                $language = $this->parseLocale($language);
194
            }
195 6
            if (isset($this->supportedLocales[$language])) {
196 6
                return $language;
197
            }
198
        }
199 1
        return null;
200
    }
201
202 29
    private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
203
    {
204 29
        if ($this->session !== null) {
205 4
            $this->logger->debug('Saving found locale to session.');
206 4
            $this->session->set($this->sessionName, $locale);
207
        }
208
209 29
        if ($this->cookieDuration !== null) {
210 4
            $this->logger->debug('Saving found locale to cookies.');
211 4
            $cookie = new Cookie(name: $this->cookieName, value: $locale, secure: $this->secureCookie);
212 4
            $cookie = $cookie->withMaxAge($this->cookieDuration);
213
214 4
            return $cookie->addToResponse($response);
215
        }
216
217 25
        return $response;
218
    }
219
220 14
    private function parseLocale(string $locale): string
221
    {
222 14
        foreach (self::LOCALE_SEPARATORS as $separator) {
223 14
            $separatorPosition = strpos($locale, $separator);
224 14
            if ($separatorPosition !== false) {
225 6
                return substr($locale, 0, $separatorPosition);
226
            }
227
        }
228
229 8
        return $locale;
230
    }
231
232 14
    private function isRequestIgnored(ServerRequestInterface $request): bool
233
    {
234 14
        foreach ($this->ignoredRequestUrlPatterns as $ignoredRequest) {
235 1
            if ((new WildcardPattern($ignoredRequest))->match($request->getUri()->getPath())) {
236 1
                return true;
237
            }
238
        }
239 13
        return false;
240
    }
241
242
    /**
243
     * @psalm-assert array<string, string> $supportedLocales
244
     *
245
     * @throws InvalidLocalesFormatException
246
     */
247 40
    private function assertSupportedLocalesFormat(array $supportedLocales): void
248
    {
249 40
        foreach ($supportedLocales as $code => $locale) {
250 39
            if (!is_string($code) || !is_string($locale)) {
251 4
                throw new InvalidLocalesFormatException();
252
            }
253
        }
254
    }
255
256 27
    private function getBaseUrl(): string
257
    {
258 27
        return rtrim($this->urlGenerator->getUriPrefix(), '/');
259
    }
260
261
    /**
262
     * Return new instance with supported locales specified.
263
     *
264
     * @param array $locales List of supported locales in key-value format such as `['ru' => 'ru_RU', 'uz' => 'uz_UZ']`.
265
     *
266
     * @throws InvalidLocalesFormatException
267
     */
268 1
    public function withSupportedLocales(array $locales): self
269
    {
270 1
        $this->assertSupportedLocalesFormat($locales);
271 1
        $new = clone $this;
272 1
        $new->supportedLocales = $locales;
273 1
        return $new;
274
    }
275
276
    /**
277
     * Return new instance with default locale specified.
278
     *
279
     * @param string $defaultLocale Default locale. It must be present as a key in {@see $supportedLocales}.
280
     */
281 8
    public function withDefaultLocale(string $defaultLocale): self
282
    {
283 8
        if (!array_key_exists($defaultLocale, $this->supportedLocales)) {
284 2
            throw new InvalidArgumentException('Default locale allows only keys from supported locales.');
285
        }
286
287 6
        $new = clone $this;
288 6
        $new->defaultLocale = $defaultLocale;
289 6
        return $new;
290
    }
291
292
    /**
293
     * Return new instance with the name of the query string parameter to look for locale.
294
     *
295
     * @param string $queryParameterName Name of the query string parameter.
296
     */
297 2
    public function withQueryParameterName(string $queryParameterName): self
298
    {
299 2
        $new = clone $this;
300 2
        $new->queryParameterName = $queryParameterName;
301 2
        return $new;
302
    }
303
304
    /**
305
     * Return new instance with the name of session parameter to store found locale. Effective only when {@see $session}
306
     * is not `null`.
307
     *
308
     * @param string $sessionName Name of session parameter.
309
     */
310 2
    public function withSessionName(string $sessionName): self
311
    {
312 2
        $new = clone $this;
313 2
        $new->sessionName = $sessionName;
314 2
        return $new;
315
    }
316
317
    /**
318
     * Return new instance with the name of cookie parameter to store found locale. Effective only when
319
     * {@see $cookieDuration} is not `null`.
320
     *
321
     * @param string $sessionName Name of cookie parameter.
322
     */
323 3
    public function withCookieName(string $sessionName): self
324
    {
325 3
        $new = clone $this;
326 3
        $new->cookieName = $sessionName;
327 3
        return $new;
328
    }
329
330
    /**
331
     * Return new instance with enabled or disabled detection of locale based on `Accept-Language` header.
332
     *
333
     * @param bool $enabled Whether middleware should detect locale.
334
     */
335 9
    public function withDetectLocale(bool $enabled): self
336
    {
337 9
        $new = clone $this;
338 9
        $new->detectLocale = $enabled;
339 9
        return $new;
340
    }
341
342
    /**
343
     * Return new instance with {@see WildcardPattern patterns} for ignoring requests with URLs matching.
344
     *
345
     * @param string[] $patterns Patterns.
346
     */
347 2
    public function withIgnoredRequestUrlPatterns(array $patterns): self
348
    {
349 2
        $new = clone $this;
350 2
        $new->ignoredRequestUrlPatterns = $patterns;
351 2
        return $new;
352
    }
353
354
    /**
355
     * Return new instance with enabled or disabled secure cookies.
356
     *
357
     * @param bool $secure Whether middleware should flag locale cookie as "secure."
358
     */
359 3
    public function withSecureCookie(bool $secure): self
360
    {
361 3
        $new = clone $this;
362 3
        $new->secureCookie = $secure;
363 3
        return $new;
364
    }
365
366
    /**
367
     * Return new instance with changed cookie duration.
368
     *
369
     * @param ?DateInterval $cookieDuration Locale cookie lifetime. When set to `null`, saving locale to cookies is
370
     * disabled completely.
371
     */
372 5
    public function withCookieDuration(?DateInterval $cookieDuration): self
373
    {
374 5
        $new = clone $this;
375 5
        $new->cookieDuration = $cookieDuration;
376 5
        return $new;
377
    }
378
379
    /**
380
     * Return new instance with changed session.
381
     *
382
     * @param SessionInterface|null $session Session instance. When set to `null`, saving locale to session is disabled
383
     * completely.
384
     */
385 5
    public function withSession(?SessionInterface $session): self
386
    {
387 5
        $new = clone $this;
388 5
        $new->session = $session;
389 5
        return $new;
390
    }
391
}
392