لارافل. التوجيه المترجمة

KDPV


مرحبا يا هبر!


UPD
في هذا المورد ، قد تتحول أهمية المقالة إلى صفر مرة تعليق واحد . يمكن حل المهمة الموضحة في المقالة بألم أقل بواسطة مكتبة تعريب mcamara / laravel .
شكرا لأجل الطرف DExploN !

كات رفعت. ضرب ضرب هو القاع.

أريد أن أخبركم عن مشكلة في التوجيه في مشروع واحد وكيف تم حلها.


في البداية ، كان مشروعنا هو الموقع الأكثر شيوعًا. كان الموقع قيد التطوير ، وكان الجمهور يتوسع ، وظهرت الحاجة لدعم تعدد اللغات. استند المشروع إلى إطار Laravel ولم تكن هناك مشاكل في تعدد اللغات (تم سحب اللغة المطلوبة من الجلسة ، أو تم اختيار اللغة الافتراضية). لقد كتبنا ترجمات ، ومفاتيح الترجمة الموصوفة بدلاً من العبارات المشفرة ، واستغلنا الميزات التالية.


المشكلة


في مرحلة ما ، أدرك فريق كبار المسئولين الاقتصاديين أن هذا النهج يتداخل مع ترتيب الموقع. ثم تلقى فريق التطوير أمرًا لإضافة مجلدات فرعية للغة إلى عنوان URL ، باستثناء اللغة الافتراضية. اتخذت طرقنا عن النموذج التالي:


صفحةروت رو (اللغة الافتراضية)الطريق أونالاب روت
عنا/o-nas/en/about-us/fr/a-propos-de-nous
تفاصيل الاتصال/kontakty/en/contacts/fr/coordonnees
أخبار/novosti/en/news/fr/les-nouvelles

كل شيء وقع في مكانه وقمنا مرة أخرى بوضع ميزات جديدة.
بعد ذلك بقليل ، كانت هناك حاجة لنشر التطبيق على العديد من المجالات. بشكل عام ، تحتوي هذه المواقع على قاعدة بيانات واحدة ، ولكن قد تختلف بعض الإعدادات حسب المجال.
قد تكون بعض المواقع متعددة اللغات (علاوة على ذلك ، مع مجموعة محدودة من اللغات ، وليس مع جميع اللغات المدعومة) ، وبعضها - لغة واحدة فقط.


تقرر معالجة جميع المجالات باستخدام تطبيق واحد (يقوم nginx بتعيين جميع المجالات إلى مصدر واحد).


يجب تكوين مجموعة اللغات التي يدعمها موقع معين واللغة الافتراضية في لوحة المسؤول ، والتي هي جذر الإصدار الذي تم اختراقه لمتغيرات config / env. أصبح من الواضح أن الحل الحالي لا يلبي قائمة الأمنيات لدينا.


قرار


لتبسيط الصورة وإظهار الحل ، قمت بنشر مشروع جديد على الإصدار 6.2 من laravel ورفضت استخدام قاعدة البيانات. في الإصدارات 5.x ، تكون الاختلافات طفيفة (ولكن بالطبع لن أرسمها).

رمز المشروع متاح على جيثب

أولاً ، نحتاج إلى تحديد جميع اللغات المدعومة في تكوين التطبيق.


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

أضف خدمتنا إلى الحاوية


التطبيق / مقدمي / 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'); // ... 

يتم تأطير أجزاء من الطرق المراد ترجمتها بواسطة ناقلات مزدوجة ( -- ). هذه هي أقنعة للاستبدال. الآن تكوين هذه الأقنعة.


التكوين / route.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', ], ], ]; 

لعرض مكون اختيار اللغة ، نحتاج إلى نقل اللغات التي يدعمها الموقع فقط إلى القالب. دعنا نكتب الوسيطة لهذا ...


المتشعب / الوسيطة / 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()); } } 

... واستبدال الطبقات المخصصة لدينا.


bootstrap / 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\ViewData إلى مجموعة web


التطبيق / المتشعب / 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 أمر وحدة التحكم route:cache


app / Custom / Illuminate / Foundation / Console / 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:clear ببساطة يحذف ملف ذاكرة التخزين المؤقت ، ولن نلمسه. لكن route:list الأوامر الآن لا تتداخل مع خيار locale .


app / Custom / Illuminate / Foundation / Console / 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() طريقة 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/ar481726/


All Articles