
مرحبا يا هبر!
UPD
في هذا المورد ، قد تتحول أهمية المقالة إلى صفر مرة تعليق واحد . يمكن حل المهمة الموضحة في المقالة بألم أقل بواسطة مكتبة تعريب mcamara / laravel .
شكرا لأجل الطرف DExploN !
كات رفعت. ضرب ضرب هو القاع.
أريد أن أخبركم عن مشكلة في التوجيه في مشروع واحد وكيف تم حلها.
في البداية ، كان مشروعنا هو الموقع الأكثر شيوعًا. كان الموقع قيد التطوير ، وكان الجمهور يتوسع ، وظهرت الحاجة لدعم تعدد اللغات. استند المشروع إلى إطار Laravel ولم تكن هناك مشاكل في تعدد اللغات (تم سحب اللغة المطلوبة من الجلسة ، أو تم اختيار اللغة الافتراضية). لقد كتبنا ترجمات ، ومفاتيح الترجمة الموصوفة بدلاً من العبارات المشفرة ، واستغلنا الميزات التالية.
المشكلة
في مرحلة ما ، أدرك فريق كبار المسئولين الاقتصاديين أن هذا النهج يتداخل مع ترتيب الموقع. ثم تلقى فريق التطوير أمرًا لإضافة مجلدات فرعية للغة إلى عنوان URL ، باستثناء اللغة الافتراضية. اتخذت طرقنا عن النموذج التالي:
كل شيء وقع في مكانه وقمنا مرة أخرى بوضع ميزات جديدة.
بعد ذلك بقليل ، كانت هناك حاجة لنشر التطبيق على العديد من المجالات. بشكل عام ، تحتوي هذه المواقع على قاعدة بيانات واحدة ، ولكن قد تختلف بعض الإعدادات حسب المجال.
قد تكون بعض المواقع متعددة اللغات (علاوة على ذلك ، مع مجموعة محدودة من اللغات ، وليس مع جميع اللغات المدعومة) ، وبعضها - لغة واحدة فقط.
تقرر معالجة جميع المجالات باستخدام تطبيق واحد (يقوم nginx بتعيين جميع المجالات إلى مصدر واحد).
يجب تكوين مجموعة اللغات التي يدعمها موقع معين واللغة الافتراضية في لوحة المسؤول ، والتي هي جذر الإصدار الذي تم اختراقه لمتغيرات config / env. أصبح من الواضح أن الحل الحالي لا يلبي قائمة الأمنيات لدينا.
قرار
لتبسيط الصورة وإظهار الحل ، قمت بنشر مشروع جديد على الإصدار 6.2 من laravel ورفضت استخدام قاعدة البيانات. في الإصدارات 5.x ، تكون الاختلافات طفيفة (ولكن بالطبع لن أرسمها).
رمز المشروع متاح على جيثب
أولاً ، نحتاج إلى تحديد جميع اللغات المدعومة في تكوين التطبيق.
نحتاج إلى جوهر 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' => [
أضف خدمتنا إلى الحاوية
التطبيق / مقدمي / AppServiceProvider.php <?php namespace App\Providers; use App\Contracts\SiteDetector; use App\Services\SiteDetector\FakeSiteDetector; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider {
الآن تحديد الطرق.
يتم تأطير أجزاء من الطرق المراد ترجمتها بواسطة ناقلات مزدوجة ( --
). هذه هي أقنعة للاستبدال. الآن تكوين هذه الأقنعة.
التكوين / route.php <?php return [ 'web.about' => [
لعرض مكون اختيار اللغة ، نحتاج إلى نقل اللغات التي يدعمها الموقع فقط إلى القالب. دعنا نكتب الوسيطة لهذا ...
المتشعب / الوسيطة / 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
التطبيق / المتشعب / Kernel.php <?php namespace App\Http;
إذا لم تقم بتخزين المسارات مؤقتًا ، فيمكنك العمل بالفعل. لكن في معركة بدون ذاكرة تخزين مؤقت ، فإن الفكرة ليست واحدة من الأفضل. لقد قمنا بالفعل بتدريس طلبنا لتلقي الطرق من ذاكرة التخزين المؤقت ، والآن نحن بحاجة إلى تعليم كيفية حفظه بشكل صحيح. تخصيص 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 { 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: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 { 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 { 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 | +--------+----------+------------------+--------------+-------------------------+------------+
هذا كل شيء. شكرا لاهتمامكم!