477 lines
15 KiB
PHP
477 lines
15 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Esun3d\LanguageUrlStoreResolver\Plugin;
|
|
|
|
use Magento\Framework\App\FrontControllerInterface;
|
|
use Magento\Framework\App\Request\Http as HttpRequest;
|
|
use Magento\Framework\App\RequestInterface;
|
|
use Magento\Framework\App\Response\Http as HttpResponse;
|
|
use Magento\Store\Model\StoreManagerInterface;
|
|
use Psr\Log\LoggerInterface;
|
|
use Esun3d\LanguageUrlStoreResolver\Model\Config;
|
|
use Esun3d\LanguageUrlStoreResolver\Model\LanguageResolver;
|
|
use Esun3d\LanguageUrlStoreResolver\Model\MarketCookie;
|
|
use Esun3d\LanguageUrlStoreResolver\Model\MarketResolver;
|
|
use Esun3d\LanguageUrlStoreResolver\Model\RequestPathProcessor;
|
|
use Esun3d\LanguageUrlStoreResolver\Model\StoreCodeMapper;
|
|
use Magento\Framework\Locale\ResolverInterface as LocaleResolverInterface;
|
|
use Magento\Framework\App\Config\ScopeConfigInterface;
|
|
use Magento\Store\Model\ScopeInterface;
|
|
|
|
class LanguageUrlFrontControllerPlugin
|
|
{
|
|
/**
|
|
* @var Config
|
|
*/
|
|
private $config;
|
|
|
|
/**
|
|
* @var LanguageResolver
|
|
*/
|
|
private $languageResolver;
|
|
|
|
/**
|
|
* @var MarketResolver
|
|
*/
|
|
private $marketResolver;
|
|
|
|
/**
|
|
* @var StoreCodeMapper
|
|
*/
|
|
private $storeCodeMapper;
|
|
|
|
/**
|
|
* @var RequestPathProcessor
|
|
*/
|
|
private $requestPathProcessor;
|
|
|
|
/**
|
|
* @var StoreManagerInterface
|
|
*/
|
|
private $storeManager;
|
|
|
|
/**
|
|
* @var MarketCookie
|
|
*/
|
|
private $marketCookie;
|
|
|
|
/**
|
|
* @var HttpResponse
|
|
*/
|
|
private $response;
|
|
|
|
/**
|
|
* @var LoggerInterface
|
|
*/
|
|
private $logger;
|
|
|
|
/**
|
|
* @var LocaleResolverInterface
|
|
*/
|
|
private $localeResolver;
|
|
|
|
/**
|
|
* @var ScopeConfigInterface
|
|
*/
|
|
private $scopeConfig;
|
|
|
|
public function __construct(
|
|
Config $config,
|
|
LanguageResolver $languageResolver,
|
|
MarketResolver $marketResolver,
|
|
StoreCodeMapper $storeCodeMapper,
|
|
RequestPathProcessor $requestPathProcessor,
|
|
StoreManagerInterface $storeManager,
|
|
MarketCookie $marketCookie,
|
|
HttpResponse $response,
|
|
LoggerInterface $logger,
|
|
LocaleResolverInterface $localeResolver,
|
|
ScopeConfigInterface $scopeConfig
|
|
) {
|
|
$this->config = $config;
|
|
$this->languageResolver = $languageResolver;
|
|
$this->marketResolver = $marketResolver;
|
|
$this->storeCodeMapper = $storeCodeMapper;
|
|
$this->requestPathProcessor = $requestPathProcessor;
|
|
$this->storeManager = $storeManager;
|
|
$this->marketCookie = $marketCookie;
|
|
$this->response = $response;
|
|
$this->logger = $logger;
|
|
$this->localeResolver = $localeResolver;
|
|
$this->scopeConfig = $scopeConfig;
|
|
}
|
|
|
|
/**
|
|
* @param callable $proceed
|
|
* @return mixed
|
|
*/
|
|
public function aroundDispatch(
|
|
FrontControllerInterface $subject,
|
|
callable $proceed,
|
|
RequestInterface $request
|
|
) {
|
|
if (!$this->config->isEnabled() || !$request instanceof HttpRequest) {
|
|
return $proceed($request);
|
|
}
|
|
|
|
$pathInfo = $request->getPathInfo();
|
|
if ($this->config->shouldSkipPath($pathInfo)) {
|
|
$this->resolveStoreForContextOnlyPath($request);
|
|
|
|
return $proceed($request);
|
|
}
|
|
|
|
$realStoreCodeRedirect = $this->getRealStoreCodeRedirect($request);
|
|
if ($realStoreCodeRedirect !== null) {
|
|
return $realStoreCodeRedirect;
|
|
}
|
|
|
|
if ($this->hasInvalidMarketParam($request)) {
|
|
return $proceed($request);
|
|
}
|
|
|
|
$marketParamRedirect = $this->getMarketParamRedirect($request);
|
|
if ($marketParamRedirect !== null) {
|
|
return $marketParamRedirect;
|
|
}
|
|
|
|
$language = $this->languageResolver->resolveFromPath($pathInfo);
|
|
if ($language === null) {
|
|
$this->resolveDefaultStoreForUnprefixedPath($request);
|
|
|
|
return $proceed($request);
|
|
}
|
|
|
|
if ($this->storeCodeMapper->isDefaultLanguage($language)) {
|
|
return $this->redirect(
|
|
$this->buildRedirectUrl($request, $this->requestPathProcessor->stripLanguagePrefix($pathInfo)),
|
|
301
|
|
);
|
|
}
|
|
|
|
$market = null;
|
|
$storeCode = null;
|
|
|
|
try {
|
|
$market = $this->marketResolver->resolveForLanguage($request, $language);
|
|
$storeCode = $this->storeCodeMapper->map($language, $market);
|
|
|
|
if ($storeCode === null) {
|
|
return $proceed($request);
|
|
}
|
|
|
|
if ($market === null) {
|
|
$market = $storeCode;
|
|
}
|
|
|
|
$this->storeManager->setCurrentStore($storeCode);
|
|
$this->applyLocaleForStore($request, $storeCode);
|
|
|
|
$configLocale = (string) $this->scopeConfig->getValue(
|
|
'general/locale/code',
|
|
ScopeInterface::SCOPE_STORE,
|
|
$storeCode
|
|
);
|
|
|
|
#$this->logger->debug('[LanguageUrlStoreResolver] locale check after set store', [
|
|
# 'path' => $request->getPathInfo(),
|
|
# 'store_code' => $storeCode,
|
|
# 'config_locale' => $configLocale,
|
|
# 'resolver_locale' => $this->localeResolver->getLocale(),
|
|
#]);
|
|
|
|
// 写入当前 storeCode, 后续恢复 store context。
|
|
$this->marketCookie->set($storeCode);
|
|
|
|
$request->setPathInfo($this->requestPathProcessor->stripLanguagePrefix($pathInfo));
|
|
$this->setRequestValue($request, 'language_url_prefix', $language);
|
|
$this->setRequestValue($request, 'language_url_market', $market);
|
|
$this->setRequestValue($request, 'language_url_store_code', $storeCode);
|
|
} catch (\Throwable $exception) {
|
|
$this->logger->error(
|
|
'[LanguageUrlStoreResolver] Failed to resolve language URL store.',
|
|
$this->buildLogContext($request, $market, $storeCode, $exception)
|
|
);
|
|
}
|
|
|
|
return $proceed($request);
|
|
}
|
|
|
|
private function getRealStoreCodeRedirect(HttpRequest $request): ?HttpResponse
|
|
{
|
|
$pathInfo = $request->getPathInfo();
|
|
$segments = explode('/', trim($pathInfo, '/'), 2);
|
|
$storeCode = $segments[0] ?? '';
|
|
|
|
if (!$this->storeCodeMapper->isRealStoreCodeInScope($storeCode)) {
|
|
return null;
|
|
}
|
|
|
|
$language = $this->storeCodeMapper->languageFromStoreCode($storeCode);
|
|
$market = $this->storeCodeMapper->marketFromStoreCode($storeCode);
|
|
|
|
if ($language === null || $market === null) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$this->marketCookie->set($market);
|
|
|
|
$remainingPath = $segments[1] ?? '';
|
|
$targetPath = '/' . $language . ($remainingPath !== '' ? '/' . ltrim($remainingPath, '/') : '');
|
|
|
|
return $this->redirect($this->buildRedirectUrl($request, $targetPath), 301);
|
|
} catch (\Throwable $exception) {
|
|
$this->logger->error(
|
|
'[LanguageUrlStoreResolver] Failed to redirect real store code URL.',
|
|
$this->buildLogContext($request, $market, $storeCode, $exception)
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function getMarketParamRedirect(HttpRequest $request): ?HttpResponse
|
|
{
|
|
$pathInfo = $request->getPathInfo();
|
|
$market = $this->marketResolver->normalizeMarket($request->getParam('market'));
|
|
if ($market === null) {
|
|
return null;
|
|
}
|
|
|
|
$targetLanguage = $this->storeCodeMapper->languageFromStoreCode($market);
|
|
if ($targetLanguage === null) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
$this->marketCookie->set($market);
|
|
$language = $this->languageResolver->resolveFromPath($pathInfo);
|
|
if ($language === null) {
|
|
if (!$this->isRootPath($pathInfo)) {
|
|
return null;
|
|
}
|
|
|
|
$targetPath = $this->storeCodeMapper->isDefaultLanguage($targetLanguage)
|
|
? '/'
|
|
: '/' . $targetLanguage . '/';
|
|
} else {
|
|
$targetPath = $this->storeCodeMapper->isDefaultLanguage($targetLanguage)
|
|
? $this->requestPathProcessor->stripLanguagePrefix($pathInfo)
|
|
: $this->requestPathProcessor->replaceLanguagePrefix($pathInfo, $targetLanguage);
|
|
}
|
|
|
|
return $this->redirect($this->buildRedirectUrl($request, $targetPath), 302);
|
|
} catch (\Throwable $exception) {
|
|
$this->logger->error(
|
|
'[LanguageUrlStoreResolver] Failed to redirect market switch URL.',
|
|
$this->buildLogContext($request, $market, $market, $exception)
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function isRootPath(string $pathInfo): bool
|
|
{
|
|
return trim($pathInfo, '/') === '';
|
|
}
|
|
|
|
private function resolveDefaultStoreForUnprefixedPath(HttpRequest $request): void
|
|
{
|
|
$storeCode = null;
|
|
|
|
try {
|
|
$defaultStore = $this->storeManager->getWebsite()->getDefaultStore();
|
|
if (!$defaultStore || !$defaultStore->isActive()) {
|
|
return;
|
|
}
|
|
|
|
$storeCode = (string)$defaultStore->getCode();
|
|
$this->storeManager->setCurrentStore($storeCode);
|
|
$this->applyLocaleForStore($request, $storeCode);
|
|
$this->marketCookie->set($storeCode);
|
|
|
|
$this->setRequestValue($request, 'language_url_market', $storeCode);
|
|
$this->setRequestValue($request, 'language_url_store_code', $storeCode);
|
|
} catch (\Throwable $exception) {
|
|
$this->logger->error(
|
|
'[LanguageUrlStoreResolver] Failed to resolve the default store for the root URL.',
|
|
$this->buildLogContext($request, $storeCode, $storeCode, $exception)
|
|
);
|
|
}
|
|
}
|
|
|
|
private function hasInvalidMarketParam(HttpRequest $request): bool
|
|
{
|
|
$market = $request->getParam('market');
|
|
|
|
if ($market === null || $market === '') {
|
|
return false;
|
|
}
|
|
|
|
return $this->marketResolver->normalizeMarket($market) === null;
|
|
}
|
|
|
|
private function resolveStoreForContextOnlyPath(HttpRequest $request): void
|
|
{
|
|
$storeCode = null;
|
|
|
|
try {
|
|
$cookieMarket = $this->marketCookie->get();
|
|
|
|
if (is_string($cookieMarket) && $cookieMarket !== '') {
|
|
$normalizedMarket = $this->marketResolver->normalizeMarket($cookieMarket);
|
|
|
|
if ($normalizedMarket !== null && $this->storeCodeMapper->isRealStoreCodeInScope($normalizedMarket)) {
|
|
$storeCode = $normalizedMarket;
|
|
}
|
|
}
|
|
|
|
if ($storeCode === null) {
|
|
$storeCode = $this->resolveStoreCodeFromReferer($request);
|
|
}
|
|
|
|
if ($storeCode === null) {
|
|
$defaultStore = $this->storeManager->getWebsite()->getDefaultStore();
|
|
$storeCode = $defaultStore && $defaultStore->isActive()
|
|
? (string)$defaultStore->getCode()
|
|
: null;
|
|
}
|
|
|
|
if ($storeCode === null || !$this->storeCodeMapper->isRealStoreCodeInScope($storeCode)) {
|
|
return;
|
|
}
|
|
|
|
$this->storeManager->setCurrentStore($storeCode);
|
|
$this->applyLocaleForStore($request, $storeCode);
|
|
$this->marketCookie->set($storeCode);
|
|
|
|
$this->setRequestValue($request, 'language_url_market', $storeCode);
|
|
$this->setRequestValue($request, 'language_url_store_code', $storeCode);
|
|
|
|
$this->logger->debug('[LanguageUrlStoreResolver] context-only path store resolved', [
|
|
'path' => $request->getPathInfo(),
|
|
'request_uri' => $request->getRequestUri(),
|
|
'store_code' => $storeCode,
|
|
]);
|
|
} catch (\Throwable $exception) {
|
|
$this->logger->error(
|
|
'[LanguageUrlStoreResolver] Failed to resolve context-only path store.',
|
|
$this->buildLogContext($request, null, $storeCode, $exception)
|
|
);
|
|
}
|
|
}
|
|
|
|
private function resolveStoreCodeFromReferer(HttpRequest $request): ?string
|
|
{
|
|
$referer = (string) $request->getServer('HTTP_REFERER');
|
|
|
|
if ($referer === '') {
|
|
return null;
|
|
}
|
|
|
|
$refererPath = (string) parse_url($referer, PHP_URL_PATH);
|
|
|
|
if ($refererPath === '') {
|
|
return null;
|
|
}
|
|
|
|
$language = $this->languageResolver->resolveFromPath($refererPath);
|
|
|
|
if ($language === null) {
|
|
return null;
|
|
}
|
|
|
|
return $this->storeCodeMapper->getDefaultStoreCodeForLanguage($language);
|
|
}
|
|
|
|
private function applyLocaleForStore(HttpRequest $request, string $storeCode): void
|
|
{
|
|
$configLocale = (string) $this->scopeConfig->getValue(
|
|
'general/locale/code',
|
|
ScopeInterface::SCOPE_STORE,
|
|
$storeCode
|
|
);
|
|
|
|
if ($configLocale === '') {
|
|
$this->logger->debug('[LanguageUrlStoreResolver] empty locale config', [
|
|
'path' => $request->getPathInfo(),
|
|
'store_code' => $storeCode,
|
|
]);
|
|
|
|
return;
|
|
}
|
|
|
|
$this->localeResolver->setLocale($configLocale);
|
|
|
|
$this->logger->debug('[LanguageUrlStoreResolver] locale applied after set store', [
|
|
'path' => $request->getPathInfo(),
|
|
'store_code' => $storeCode,
|
|
'config_locale' => $configLocale,
|
|
'resolver_locale' => $this->localeResolver->getLocale(),
|
|
]);
|
|
}
|
|
|
|
private function redirect(string $url, int $statusCode): HttpResponse
|
|
{
|
|
$this->response->setRedirect($url, $statusCode);
|
|
|
|
return $this->response;
|
|
}
|
|
|
|
private function buildRedirectUrl(HttpRequest $request, string $path): string
|
|
{
|
|
$baseUrl = method_exists($request, 'getBaseUrl') ? rtrim((string) $request->getBaseUrl(), '/') : '';
|
|
$queryString = $this->cleanQueryString((string) parse_url($request->getRequestUri(), PHP_URL_QUERY));
|
|
$url = $baseUrl . '/' . ltrim($path, '/');
|
|
|
|
if ($queryString !== '') {
|
|
$url .= '?' . $queryString;
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
private function cleanQueryString(string $queryString): string
|
|
{
|
|
if ($queryString === '') {
|
|
return '';
|
|
}
|
|
|
|
parse_str($queryString, $params);
|
|
|
|
unset($params['market'], $params['store'], $params['___store']);
|
|
|
|
return http_build_query($params);
|
|
}
|
|
|
|
private function setRequestValue(HttpRequest $request, string $key, string $value): void
|
|
{
|
|
$request->setParam($key, $value);
|
|
|
|
if (method_exists($request, 'setAlias')) {
|
|
$request->setAlias($key, $value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return mixed[]
|
|
*/
|
|
private function buildLogContext(
|
|
HttpRequest $request,
|
|
?string $market,
|
|
?string $storeCode,
|
|
\Throwable $exception
|
|
): array {
|
|
return [
|
|
'path' => $request->getPathInfo(),
|
|
'request_uri' => $request->getRequestUri(),
|
|
'market' => $market,
|
|
'store_code' => $storeCode,
|
|
'exception' => $exception,
|
|
];
|
|
}
|
|
}
|