拉拉韦尔 本地化路由

KDPV


哈Ha!


UPD
在此资源上,一篇文章的相关性可能是零乘以一条评论 。 本文中描述的任务可以通过mcamara / laravel-localization轻松解决。
感谢您的提示DExploN

吉提出。 底数乘以零。

我想告诉您有关一个项目中的路由问题以及我们如何解决的问题。


最初,我们的项目是最常见的站点。 该网站正在发展,受众不断扩大,并且需要支持多种语言。 该项目基于Laravel框架,使用多种语言没有问题(从会话中提取了所需的语言,或者使用了默认语言)。 我们编写了翻译,规定了翻译键,而不是硬编码的短语,并采用了以下功能。


问题


在某些时候,SEO团队意识到这种方法会干扰网站的排名。 然后,开发团队收到一条命令,用于向URL添加语言子文件夹(默认语言除外)。 我们的路线大致采用以下形式:


页数Rout ru(默认语言)路线en卢特fr
关于我们/o-nas/en/about-us/fr/a-propos-de-nous
联络资料/kontakty/en/contacts/fr/coordonnees
最新消息/novosti/en/news/fr/les-nouvelles

一切都准备就绪,我们再次着手新功能。
稍后,需要在多个域上部署该应用程序。 通常,这些站点只有一个数据库,但是某些设置可能会因域而异。
某些网站可能是多语言的(此外,使用的语言集有限,而并非所有受支持的语言),有些则只能是一种语言。


决定使用一个应用程序处理所有域(nginx将所有域代理到一个上游)。


特定站点支持的语言集和默认语言应在管理面板中配置,该面板是config / env变量被黑版的根。 很明显,当前的解决方案无法满足我们的愿望清单。


解决方案


为了简化图片并演示解决方案,我在laravel 6.2版上部署了一个新项目,并拒绝使用该数据库。 在版本5.x中,差异很小(但是我不会画它们)。

项目代码可在GitHub上获得

首先,我们需要在应用程序配置中指定所有支持的语言。


config / app.php
 <?php return [ // ... 'locale' => 'en', 'fallback_locale' => 'en', 'supported_locales' => [ 'en', 'ru', 'de', 'fr', ], // ... ]; 

我们需要Site站点的实质以及确定站点设置的服务。


应用程序/实体/ Site.php
 <?php declare(strict_types=1); namespace App\Entities; class Site { /** * @var string   */ private $domain; /** * @var string    */ private $defaultLanguage; /** * @var string[]    */ private $supportedLanguages = []; /** * @param string $domain  * @param string $defaultLanguage    * @param string[] $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; } /** *    * * @return string */ public function getDomain(): string { return $this->domain; } /** *       * * @return string */ public function getDefaultLanguage(): string { return $this->defaultLanguage; } /** *      * * @return string[] */ public function getSupportedLanguages(): array { return $this->supportedLanguages; } /** *     * * @param string $language * @return bool */ public function isLanguageSupported(string $language): bool { return in_array($language, $this->supportedLanguages); } /** * ,      * * @param string $language * @return bool */ 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 { /** *     * * @param string $host  * * @return Site   * * @throws NotFoundHttpException     */ 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 { /** * @var Site[]  */ private $sites; public function __construct() { $sites = [ 'localhost' => [ //   'default' => 'en', 'support' => ['ru', 'de', 'fr'], ], 'site-all.local' => [ //   'default' => 'en', 'support' => ['ru', 'de', 'fr'], ], 'site-ru.local' => [ //   'default' => 'ru', 'support' => [], ], 'site-en.local' => [ 'default' => 'en', //   'support' => [], ], 'site-de.local' => [ 'default' => 'de', //   'support' => [], ], 'site-fr.local' => [ 'default' => 'fr', //   'support' => [], ], 'site-eur.local' => [ //    'default' => 'de', 'support' => ['fr'], ], ]; foreach ($sites as $domain => $site) { $default = $site['default']; $support = array_merge([$default], $site['support']); $this->sites[$domain] = new Site($domain, $default, $support); } } public function detect(string $host): Site { $host = trim(mb_strtolower($host)); if (!array_key_exists($host, $this->sites)) { throw new NotFoundHttpException(); } return $this->sites[$host]; } } 

将我们的服务添加到容器中


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 { // ... /** * Register any application services. * * @return void */ public function register() { // ... /* *  . */ $this->app->singleton(FakeSiteDetector::class, function () { return new FakeSiteDetector(); }); /* *   */ $this->app->bind(SiteDetector::class, FakeSiteDetector::class); // ... } // ... } 

现在定义路线。


路线/ web.php
 <?php // ... Route::get('/', 'DemoController@home')->name('web.home'); Route::get('/--about--', 'DemoController@about')->name('web.about'); Route::get('/--contacts--', 'DemoController@contacts')->name('web.contacts'); Route::get('/--news--', 'DemoController@news')->name('web.news'); // ... 

待本地化的路线的某些部分用双减号( -- )框起来。 这些是可更换的口罩。 现在配置这些掩码。


配置/routes.php
 <?php return [ 'web.about' => [ //   'about' => [ //     'de' => 'uber-uns', //  =>  'en' => 'about-us', 'fr' => 'a-propos-de-nous', 'ru' => 'o-nas', ], ], 'web.news' => [ 'news' => [ 'de' => 'nachrichten', 'en' => 'news', 'fr' => 'nouvelles', 'ru' => 'novosti', ], ], 'web.contacts' => [ 'contacts' => [ 'de' => 'kontakte', 'en' => 'contacts', 'fr' => 'contacts', 'ru' => 'kontakty', ], ], ]; 

要显示语言选择组件,我们只需要将网站支持的那些语言传输到模板。 让我们为此编写一个中间件...


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 { /** * @var ViewFactory */ private $view; /** * @var SiteDetector */ 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 { /** * @var array   . */ private $config; private $localized = []; public function setConfig(array $config) { $this->config = $config; } /** *    . * * @param string $language  */ 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); } } /** * @inheritDoc */ public function serialize() { return serialize([ 'routes' => $this->routes, 'allRoutes' => $this->allRoutes, 'nameList' => $this->nameList, 'actionList' => $this->actionList, ]); } /** * @inheritDoc */ 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; } /** @var Repository $config */ $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); /** *         *       * * @var RouteCollection $routes */ $routes = $this->get('router')->getRoutes(); $routes->setConfig($this->get('config')->get('routes')); if ($this->routesAreCached()) { /** @noinspection PhpIncludeInspection */ $this->cachedRoutes = require $this->getCachedRoutesPath(); } $this->setLocale($this->getLocale()); } } 

...并替换我们的自定义类。


引导程序/ app.php
 <?php // $app = new Illuminate\Foundation\Application($_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)); $app = new App\Custom\Illuminate\Foundation\Application($_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)); $app->get('router')->setRoutes(new App\Custom\Illuminate\Routing\RouteCollection()); // ... 

下一步是从URL地址的第一部分确定语言。 为此,在分派之前,我们将获得它的第一个细分,检查网站对这种语言的支持,并在没有此细分的情况下以新的请求开始进行分配。 让我们稍微修复一下类App\Http\Kernel ,然后将我们的中间件App\Http\Middleware\ViewDataweb组。


应用程序/ Http / Kernel.php
 <?php namespace App\Http; // ... use App\Contracts\SiteDetector; use App\Http\Middleware\ViewData; use Closure; use Illuminate\Foundation\Http\Kernel as HttpKernel; use Illuminate\Http\Request; // ... class Kernel extends HttpKernel { // ... /** * The application's route middleware groups. * * @var array */ protected $middlewareGroups = [ // ... 'web' => [ // ... ViewData::class, ], // ... ]; // ... /** * Get the route dispatcher callback. * * @return Closure */ protected function dispatchToRouter() { return function (Request $request) { /* *   */ /** @var SiteDetector $siteDetector */ $siteDetector = $this->app->get(SiteDetector::class); $site = $siteDetector->detect($request->getHost()); /* *     */ $segment = (string)$request->segment(1); /* *           ,    */ if ($segment && $site->isLanguageSupported($segment)) { $language = $segment; } else { $language = $site->getDefaultLanguage(); } /* *      */ $this->app->get('config')->set('app.supported_locales', $site->getSupportedLanguages()); /* *      */ $this->app->get('config')->set('app.fallback_locale', $site->getDefaultLanguage()); /* *    */ $this->app->setLocale($language); /* *           */ if (!$site->isLanguageDefault($language)) { /* *      . */ $server = $request->server(); $server['REQUEST_URI'] = mb_substr($server['REQUEST_URI'], mb_strlen($language) + 1); $request = $request->duplicate( $request->query->all(), $request->all(), $request->attributes->all(), $request->cookies->all(), $request->files->all(), $server ); } /* *   */ $this->app->instance('request', $request); return $this->router->dispatch($request); }; } } 

如果您不缓存路由,那么您已经可以工作。 但是在没有缓存的战斗中,这个主意并不是最好的。 我们已经教导我们的应用程序从缓存中接收路由,现在我们需要教导如何正确保存它。 自定义控制台命令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 { /** * Execute the console command. * * @return void */ 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) { /* *        . * *   - ,  - ,    Illuminate\Routing\RouteCollection */ $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) { /** @var CustomRouteCollection|Route[] $routes */ $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 { /** * Execute the console command. * * @return void */ 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 { /** * Register any application services. * * @return void */ 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', ]; } } 

当然,我们将提供程序添加到配置中。


config / app.php
 <?php return [ // ... 'providers' => [ App\Providers\CommandsReplaceProvider::class, ], // ... ]; 

现在一切正常!


 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 | +--------+----------+------------------+--------------+-------------------------+------------+ 

仅此而已。 感谢您的关注!

Source: https://habr.com/ru/post/zh-CN481726/


All Articles