
哈Ha!
UPD
在此资源上,一篇文章的相关性可能是零乘以一条评论 。 本文中描述的任务可以通过mcamara / laravel-localization库轻松解决。
感谢您的提示DExploN !
吉提出。 底数乘以零。
我想告诉您有关一个项目中的路由问题以及我们如何解决的问题。
最初,我们的项目是最常见的站点。 该网站正在发展,受众不断扩大,并且需要支持多种语言。 该项目基于Laravel框架,使用多种语言没有问题(从会话中提取了所需的语言,或者使用了默认语言)。 我们编写了翻译,规定了翻译键,而不是硬编码的短语,并采用了以下功能。
问题
在某些时候,SEO团队意识到这种方法会干扰网站的排名。 然后,开发团队收到一条命令,用于向URL添加语言子文件夹(默认语言除外)。 我们的路线大致采用以下形式:
一切都准备就绪,我们再次着手新功能。
稍后,需要在多个域上部署该应用程序。 通常,这些站点只有一个数据库,但是某些设置可能会因域而异。
某些网站可能是多语言的(此外,使用的语言集有限,而并非所有受支持的语言),有些则只能是一种语言。
决定使用一个应用程序处理所有域(nginx将所有域代理到一个上游)。
特定站点支持的语言集和默认语言应在管理面板中配置,该面板是config / env变量被黑版的根。 很明显,当前的解决方案无法满足我们的愿望清单。
解决方案
为了简化图片并演示解决方案,我在laravel 6.2版上部署了一个新项目,并拒绝使用该数据库。 在版本5.x中,差异很小(但是我不会画它们)。
项目代码可在GitHub上获得
首先,我们需要在应用程序配置中指定所有支持的语言。
我们需要Site
站点的实质以及确定站点设置的服务。
应用程序/实体/ Site.php <?php declare(strict_types=1); namespace App\Entities; class Site { private $domain; private $defaultLanguage; private $supportedLanguages = []; public function __construct(string $domain, string $defaultLanguage, array $supportedLanguages) { $this->domain = $domain; $this->defaultLanguage = $defaultLanguage; if (!in_array($defaultLanguage, $supportedLanguages)) { $supportedLanguages[] = $defaultLanguage; } $this->supportedLanguages = $supportedLanguages; } public function getDomain(): string { return $this->domain; } public function getDefaultLanguage(): string { return $this->defaultLanguage; } public function getSupportedLanguages(): array { return $this->supportedLanguages; } public function isLanguageSupported(string $language): bool { return in_array($language, $this->supportedLanguages); } public function isLanguageDefault(string $language): bool { return $language === $this->defaultLanguage; } }
应用程式/合约/ SiteDetector.php <?php declare(strict_types=1); namespace App\Contracts; use App\Entities\Site; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; interface SiteDetector { public function detect(string $host): Site; }
应用程序/服务/ SiteDetector / FakeSiteDetector.php <?php declare(strict_types=1); namespace App\Services\SiteDetector; use App\Contracts\SiteDetector; use App\Entities\Site; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class FakeSiteDetector implements SiteDetector { private $sites; public function __construct() { $sites = [ 'localhost' => [
将我们的服务添加到容器中
app / Providers / AppServiceProvider.php <?php namespace App\Providers; use App\Contracts\SiteDetector; use App\Services\SiteDetector\FakeSiteDetector; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider {
现在定义路线。
待本地化的路线的某些部分用双减号( --
)框起来。 这些是可更换的口罩。 现在配置这些掩码。
配置/routes.php <?php return [ 'web.about' => [
要显示语言选择组件,我们只需要将网站支持的那些语言传输到模板。 让我们为此编写一个中间件...
Http /中间件/ ViewData.php <?php namespace App\Http\Middleware; use App\Contracts\SiteDetector; use Closure; use Illuminate\Contracts\View\Factory as ViewFactory; use Illuminate\Contracts\View\View; use Illuminate\Http\Request; class ViewData { private $view; private $detector; public function __construct(ViewFactory $view, SiteDetector $detector) { $this->view = $view; $this->detector = $detector; } public function handle(Request $request, Closure $next) { $site = $this->detector->detect($request->getHost()); $languages = []; foreach ($site->getSupportedLanguages() as $language) { $url = '/'; if (!$site->isLanguageDefault($language)) { $url .= $language; } $languages[$language] = $url; } $this->view->composer(['components/languages'], function(View $view) use ($languages) { $view->with('languages', $languages); }); return $next($request); } }
现在,您需要自定义路由器。 相反,不是路由器本身,而是路由的集合...
应用/自定义/照亮/路由/ RouteCollection.php <?php namespace App\Custom\Illuminate\Routing; use Illuminate\Routing\Route; use Illuminate\Routing\RouteCollection as BaseRouteCollection; use Serializable; class RouteCollection extends BaseRouteCollection implements Serializable { private $config; private $localized = []; public function setConfig(array $config) { $this->config = $config; } public function localize(string $language) { $this->flushLocalizedRoutes(); foreach ($this->config as $name => $placeholders) { if (!$this->hasNamedRoute($name) || empty($placeholders)) { continue; } $route = $this->getByName($name); $this->localized[$name] = $route; $this->removeRoute($route); $new = clone $route; $uri = $new->uri(); foreach ($placeholders as $placeholder => $paths) { if (!array_key_exists($language, $paths)) { continue; } $value = $paths[$language]; $uri = str_replace('--' . $placeholder . '--', $value, $uri); } $new->setUri($uri); $this->add($new); } $this->refreshNameLookups(); $this->refreshActionLookups(); } private function removeRoute(Route $route) { $uri = $route->uri(); $domainAndUri = $route->getDomain().$uri; foreach ($route->methods() as $method) { $key = $method.$domainAndUri; if (array_key_exists($key, $this->allRoutes)) { unset($this->allRoutes[$key]); } if (array_key_exists($uri, $this->routes[$method])) { unset($this->routes[$method][$uri]); } } } private function flushLocalizedRoutes() { foreach ($this->localized as $name => $route) { $old = $this->getByName($name); $this->removeRoute($old); $this->add($route); } } public function serialize() { return serialize([ 'routes' => $this->routes, 'allRoutes' => $this->allRoutes, 'nameList' => $this->nameList, 'actionList' => $this->actionList, ]); } public function unserialize($serialized) { $data = unserialize($serialized); $this->routes = $data['routes']; $this->allRoutes = $data['allRoutes']; $this->nameList = $data['nameList']; $this->actionList = $data['actionList']; } }
...,应用程序的主要类...
应用程序/定制/照亮/基础/ Application.php <?php namespace App\Custom\Illuminate\Foundation; use App\Custom\Illuminate\Routing\RouteCollection; use App\Exceptions\UnsupportedLocaleException; use Illuminate\Contracts\Config\Repository; use Illuminate\Foundation\Application as BaseApplication; use Illuminate\Routing\UrlGenerator; class Application extends BaseApplication { private $isLocaleEstablished = false; private $cachedRoutes = []; public function __construct($basePath = null) { parent::__construct($basePath); } public function setLocale($locale) { if ($this->getLocale() === $locale && $this->isLocaleEstablished) { return; } $config = $this->get('config'); $urlGenerator = $this->get('url'); $defaultLocale = $config->get('app.fallback_locale'); $supportedLocales = $config->get('app.supported_locales'); if (!in_array($locale, $supportedLocales)) { throw new UnsupportedLocaleException(); } if ($defaultLocale !== $locale && $urlGenerator instanceof UrlGenerator) { $request = $urlGenerator->getRequest(); $rootUrl = $request->getSchemeAndHttpHost() . '/' . $locale; $urlGenerator->forceRootUrl($rootUrl); } parent::setLocale($locale); if (array_key_exists($locale, $this->cachedRoutes)) { $fn = $this->cachedRoutes[$locale]; $this->get('router')->setRoutes($fn()); } else { $this->get('router')->getRoutes()->localize($locale); } $this->isLocaleEstablished = true; } public function bootstrapWith(array $bootstrappers) { parent::bootstrapWith($bootstrappers); $routes = $this->get('router')->getRoutes(); $routes->setConfig($this->get('config')->get('routes')); if ($this->routesAreCached()) { $this->cachedRoutes = require $this->getCachedRoutesPath(); } $this->setLocale($this->getLocale()); } }
...并替换我们的自定义类。
下一步是从URL地址的第一部分确定语言。 为此,在分派之前,我们将获得它的第一个细分,检查网站对这种语言的支持,并在没有此细分的情况下以新的请求开始进行分配。 让我们稍微修复一下类App\Http\Kernel
,然后将我们的中间件App\Http\Middleware\ViewData
到web
组。
应用程序/ Http / Kernel.php <?php namespace App\Http;
如果您不缓存路由,那么您已经可以工作。 但是在没有缓存的战斗中,这个主意并不是最好的。 我们已经教导我们的应用程序从缓存中接收路由,现在我们需要教导如何正确保存它。 自定义控制台命令route:cache
应用程序/自定义/照亮/基础/控制台/ RouteCacheCommand.php <?php declare(strict_types=1); namespace App\Custom\Illuminate\Foundation\Console; use App\Custom\Illuminate\Routing\RouteCollection as CustomRouteCollection; use Illuminate\Routing\Route; use Illuminate\Routing\RouteCollection; use Illuminate\Foundation\Console\RouteCacheCommand as BaseCommand; class RouteCacheCommand extends BaseCommand { public function handle() { $this->call('route:clear'); $routes = $this->getFreshApplicationRoutes(); if (count($routes) === 0) { $this->error("Your application doesn't have any routes."); return; } $this->files->put( $this->laravel->getCachedRoutesPath(), $this->buildRouteCacheFile($routes) ); $this->info('Routes cached successfully!'); return; } protected function buildRouteCacheFile(RouteCollection $base) { $code = '<?php' . PHP_EOL . PHP_EOL; $code .= 'return [' . PHP_EOL; $stub = ' \'{{key}}\' => function() {return unserialize(base64_decode(\'{{routes}}\'));},'; foreach (config('app.supported_locales') as $locale) { $routes = clone $base; $routes->localize($locale); foreach ($routes as $route) { $route->prepareForSerialization(); } $line = str_replace('{{routes}}', base64_encode(serialize($routes)), $stub); $line = str_replace('{{key}}', $locale, $line); $code .= $line . PHP_EOL; } $code .= '];' . PHP_EOL; return $code; } }
route:clear
命令只会删除缓存文件,我们不会对其进行处理。 但是route:list
命令现在不会干扰locale
选项。
应用程序/自定义/照亮/基础/控制台/ RouteListCommand.php <?php declare(strict_types=1); namespace App\Custom\Illuminate\Foundation\Console; use Illuminate\Foundation\Console\RouteListCommand as BaseCommand; use Symfony\Component\Console\Input\InputOption; class RouteListCommand extends BaseCommand { public function handle() { $locales = $this->option('locale'); foreach ($locales as $locale) { if ($locale && in_array($locale, config('app.supported_locales'))) { $this->output->title($locale); $this->laravel->setLocale($locale); $this->router = $this->laravel->get('router'); parent::handle(); } } } protected function getOptions() { $all = config('app.supported_locales'); $result = parent::getOptions(); $result[] = ['locale', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Locales', $all]; return $result; } }
现在我们需要使这些命令起作用。 现在,供应商团队将工作。 要替换控制台命令的实现,您需要在实现Illuminate\Contracts\Support\DeferrableProvider
的应用程序中包括服务提供商。 provides()
方法应返回与命令类相对应的容器键数组。
应用程序/提供程序/ CommandsReplaceProvider.php <?php declare(strict_types=1); namespace App\Providers; use App\Custom\Illuminate\Foundation\Console\RouteCacheCommand; use App\Custom\Illuminate\Foundation\Console\RouteListCommand; use Illuminate\Contracts\Foundation\Application; use Illuminate\Contracts\Support\DeferrableProvider; use Illuminate\Support\ServiceProvider; class CommandsReplaceProvider extends ServiceProvider implements DeferrableProvider { public function register() { $this->app->singleton('command.route.cache', function (Application $app) { return new RouteCacheCommand($app->get('files')); }); $this->app->singleton('command.route.list', function (Application $app) { return new RouteListCommand($app->get('router')); }); $this->commands($this->provides()); } public function provides() { return [ 'command.route.cache', 'command.route.list', ]; } }
当然,我们将提供程序添加到配置中。
现在一切正常!
user@host laravel-localized-routing $ ./artisan route:list en == +--------+----------+----------+--------------+-------------------------+------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+----------+--------------+-------------------------+------------+ | | GET|HEAD | / | web.home | DemoController@home | web | | | GET|HEAD | about-us | web.about | DemoController@about | web | | | GET|HEAD | contacts | web.contacts | DemoController@contacts | web | | | GET|HEAD | news | web.news | DemoController@news | web | +--------+----------+----------+--------------+-------------------------+------------+ ru == +--------+----------+----------+--------------+-------------------------+------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+----------+--------------+-------------------------+------------+ | | GET|HEAD | / | web.home | DemoController@home | web | | | GET|HEAD | kontakty | web.contacts | DemoController@contacts | web | | | GET|HEAD | novosti | web.news | DemoController@news | web | | | GET|HEAD | o-nas | web.about | DemoController@about | web | +--------+----------+----------+--------------+-------------------------+------------+ de == +--------+----------+-------------+--------------+-------------------------+------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+-------------+--------------+-------------------------+------------+ | | GET|HEAD | / | web.home | DemoController@home | web | | | GET|HEAD | kontakte | web.contacts | DemoController@contacts | web | | | GET|HEAD | nachrichten | web.news | DemoController@news | web | | | GET|HEAD | uber-uns | web.about | DemoController@about | web | +--------+----------+-------------+--------------+-------------------------+------------+ fr == +--------+----------+------------------+--------------+-------------------------+------------+ | Domain | Method | URI | Name | Action | Middleware | +--------+----------+------------------+--------------+-------------------------+------------+ | | GET|HEAD | / | web.home | DemoController@home | web | | | GET|HEAD | a-propos-de-nous | web.about | DemoController@about | web | | | GET|HEAD | contacts | web.contacts | DemoController@contacts | web | | | GET|HEAD | nouvelles | web.news | DemoController@news | web | +--------+----------+------------------+--------------+-------------------------+------------+
仅此而已。 感谢您的关注!