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

Locale::checkLocales()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 4

Importance

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