Passed
Pull Request — master (#36)
by Rustam
02:59
created

Locale::withEnableDetectLocale()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 3
nc 1
nop 1
dl 0
loc 5
ccs 4
cts 4
cp 1
crap 1
rs 10
c 1
b 0
f 0
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 13
    public function __construct(
37
        private TranslatorInterface $translator,
38
        private UrlGeneratorInterface $urlGenerator,
39
        private SessionInterface $session,
40
        private Aliases $aliases,
41
        private LoggerInterface $logger,
42
        private ResponseFactoryInterface $responseFactory,
43
        private array $locales = [],
44
        private array $ignoredRequests = [],
45
        private bool $cookieSecure = false,
46
    ) {
47 13
        $this->cookieDuration = new DateInterval('P30D');
48
    }
49
50 12
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
51
    {
52 12
        if ($this->locales === []) {
53 1
            return $handler->handle($request);
54
        }
55
56 11
        $uri = $request->getUri();
57 11
        $path = $uri->getPath();
58 11
        $query = $uri->getQuery();
59
60 11
        [$locale, $country] = $this->getLocaleFromPath($path);
61
62 11
        if ($locale !== null) {
63 5
            $this->translator->setLocale($locale);
64 5
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
65
66 5
            $response = $handler->handle($request);
67 5
            $newPath = null;
68 5
            if ($this->isDefaultLocale($locale, $country) && $request->getMethod() === Method::GET) {
69 3
                $length = strlen($locale);
70 3
                $newPath = substr($path, $length + 1);
71
            }
72 5
            return $this->applyLocaleFromPath($locale, $response, $query, $newPath);
73
        }
74 6
        if ($this->enableSaveLocale) {
75 6
            [$locale, $country] = $this->getLocaleFromRequest($request);
76
        }
77 6
        if ($locale === null && $this->enableDetectLocale) {
78 2
            [$locale, $country] = $this->detectLocale($request);
79
        }
80 6
        if ($locale === null || $this->isDefaultLocale($locale, $country) || $this->isRequestIgnored($request)) {
81 2
            $this->urlGenerator->setDefaultArgument($this->queryParameterName, null);
82 2
            $request = $request->withUri($uri->withPath('/' . $this->defaultLocale . $path));
83 2
            return $handler->handle($request);
84
        }
85
86 4
        $this->translator->setLocale($locale);
87 4
        $this->urlGenerator->setDefaultArgument($this->queryParameterName, $locale);
88
89 4
        if ($request->getMethod() === Method::GET) {
90 3
            $location = rtrim($this->aliases->get('@baseUrl'), '/') . '/'
91 3
                . $locale . $path . ($query !== '' ? '?' . $query : '');
92 3
            return $this->responseFactory
93 3
                ->createResponse(Status::FOUND)
94 3
                ->withHeader(Header::LOCATION, $location);
95
        }
96
97
98 1
        return $handler->handle($request);
99
    }
100
101 5
    private function applyLocaleFromPath(
102
        string $locale,
103
        ResponseInterface $response,
104
        string $query,
105
        ?string $newPath = null,
106
    ): ResponseInterface {
107 5
        if ($newPath === '') {
108 1
            $newPath = '/';
109
        }
110
111 5
        if ($newPath !== null) {
112 3
            $location = rtrim($this->aliases->get('@baseUrl'), '/')
113 3
                . $newPath . ($query !== '' ? '?' . $query : '');
114 3
            $response = $this->responseFactory
115 3
                ->createResponse(Status::FOUND)
116 3
                ->withHeader(Header::LOCATION, $location);
117
        }
118 5
        if ($this->enableSaveLocale) {
119 5
            $response = $this->saveLocale($locale, $response);
120
        }
121 5
        return $response;
122
    }
123
124 11
    private function getLocaleFromPath(string $path): array
125
    {
126 11
        $parts = [];
127 11
        foreach ($this->locales as $code => $locale) {
128 11
            $lang = is_string($code) ? $code : $locale;
129 11
            $parts[] = $lang;
130
        }
131
132 11
        $pattern = implode('|', $parts);
133 11
        if (preg_match("#^/($pattern)\b(/?)#i", $path, $matches)) {
134 5
            $locale = $matches[1];
135 5
            [$locale, $country] = $this->parseLocale($locale);
136 5
            if (isset($this->locales[$locale])) {
137 5
                $this->logger->debug(sprintf("Locale '%s' found in URL", $locale));
138 5
                return [$locale, $country];
139
            }
140
        }
141 6
        return [null, null];
142
    }
143
144 6
    private function getLocaleFromRequest(ServerRequestInterface $request): array
145
    {
146 6
        $cookies = $request->getCookieParams();
147 6
        if (isset($cookies[$this->sessionName])) {
148 1
            $this->logger->debug(sprintf("Locale '%s' found in cookies", $cookies[$this->sessionName]));
149 1
            return $this->parseLocale($cookies[$this->sessionName]);
150
        }
151 5
        $queryParameters = $request->getQueryParams();
152 5
        if (isset($queryParameters[$this->queryParameterName])) {
153 3
            $this->logger->debug(
154 3
                sprintf("Locale '%s' found in query string", $queryParameters[$this->queryParameterName])
155 3
            );
156 3
            return $this->parseLocale($queryParameters[$this->queryParameterName]);
157
        }
158 2
        return [null, null];
159
    }
160
161 10
    private function isDefaultLocale(string $locale, ?string $country): bool
162
    {
163 10
        return $locale === $this->defaultLocale || ($country !== null && $this->defaultLocale === "$locale-$country");
164
    }
165
166 2
    private function detectLocale(ServerRequestInterface $request): array
167
    {
168 2
        foreach ($request->getHeader(Header::ACCEPT_LANGUAGE) as $language) {
169 1
            return $this->parseLocale($language);
170
        }
171 1
        return [null, null];
172
    }
173
174 5
    private function saveLocale(string $locale, ResponseInterface $response): ResponseInterface
175
    {
176 5
        $this->logger->debug('Saving found locale to cookies');
177 5
        $this->session->set($this->sessionName, $locale);
178 5
        $cookie = new Cookie(name: $this->sessionName, value: $locale, secure: $this->cookieSecure);
179 5
        if ($this->cookieDuration !== null) {
180 5
            $cookie = $cookie->withMaxAge($this->cookieDuration);
181
        }
182 5
        return $cookie->addToResponse($response);
183
    }
184
185 10
    private function parseLocale(string $locale): array
186
    {
187 10
        if (str_contains($locale, '-')) {
188
            return explode('-', $locale, 2);
189
        }
190
191 10
        if (str_contains($locale, '_')) {
192
            return explode('_', $locale, 2);
193
        }
194 10
        if (isset($this->locales[$locale]) && str_contains($this->locales[$locale], '-')) {
195 10
            return explode('-', $this->locales[$locale], 2);
196
        }
197
        return [$locale, null];
198
    }
199
200 5
    private function isRequestIgnored(ServerRequestInterface $request): bool
201
    {
202 5
        foreach ($this->ignoredRequests as $ignoredRequest) {
203 1
            if ((new WildcardPattern($ignoredRequest))->match($request->getUri()->getPath())) {
204 1
                return true;
205
            }
206
        }
207 4
        return false;
208
    }
209
210 1
    public function withLocales(array $locales): self
211
    {
212 1
        $new = clone $this;
213 1
        $new->locales = $locales;
214 1
        return $new;
215
    }
216
217 3
    public function withDefaultLocale(string $defaultLocale): self
218
    {
219 3
        $new = clone $this;
220 3
        $new->defaultLocale = $defaultLocale;
221 3
        return $new;
222
    }
223
224 1
    public function withQueryParameterName(string $queryParameterName): self
225
    {
226 1
        $new = clone $this;
227 1
        $new->queryParameterName = $queryParameterName;
228 1
        return $new;
229
    }
230
231 1
    public function withSessionName(string $sessionName): self
232
    {
233 1
        $new = clone $this;
234 1
        $new->sessionName = $sessionName;
235 1
        return $new;
236
    }
237
238 1
    public function withEnableSaveLocale(bool $enableSaveLocale): self
239
    {
240 1
        $new = clone $this;
241 1
        $new->enableSaveLocale = $enableSaveLocale;
242 1
        return $new;
243
    }
244
245 3
    public function withEnableDetectLocale(bool $enableDetectLocale): self
246
    {
247 3
        $new = clone $this;
248 3
        $new->enableDetectLocale = $enableDetectLocale;
249 3
        return $new;
250
    }
251
252 2
    public function withIgnoredRequests(array $ignoredRequests): self
253
    {
254 2
        $new = clone $this;
255 2
        $new->ignoredRequests = $ignoredRequests;
256 2
        return $new;
257
    }
258
259 1
    public function withCookieSecure(bool $secure): self
260
    {
261 1
        $new = clone $this;
262 1
        $new->cookieSecure = $secure;
263 1
        return $new;
264
    }
265
}
266