Test Failed
Pull Request — master (#36)
by Rustam
02:34
created

Locale::withBaseUrlAlias()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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