Files
esun3d/app/code/Esun3d/LanguageUrlStoreResolver/Plugin/LanguageUrlFrontControllerPlugin.php
2026-09-15 10:35:56 +08:00

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,
];
}
}