
Halo, Habr!
UPD
Pada sumber daya ini, relevansi artikel dapat berubah menjadi nol kali satu komentar . Tugas yang dijelaskan dalam artikel dapat diselesaikan dengan sedikit rasa sakit oleh perpustakaan mcamara / laravel-lokalisasi .
Terima kasih atas tipnya DExploN !
Kat mengangkat. Dikalikan dengan nol adalah bagian bawah.
Saya ingin memberi tahu Anda tentang bagaimana dalam satu proyek ada masalah dengan perutean dan bagaimana kami menyelesaikannya.
Awalnya, proyek kami adalah situs yang paling umum. Situs ini berkembang, audiens berkembang, dan kebutuhan muncul untuk mendukung multibahasa. Proyek ini didasarkan pada kerangka Laravel dan tidak ada masalah dengan multibahasa (bahasa yang diinginkan ditarik dari sesi, atau yang standar diambil). Kami menulis terjemahan, menentukan kunci terjemahan alih-alih frasa kode-keras, dan menggunakan fitur berikut untuk bekerja.
Masalah
Pada titik tertentu, tim SEO menyadari bahwa pendekatan ini mengganggu peringkat situs. Kemudian tim pengembangan menerima perintah untuk menambahkan subfolder bahasa ke URL, kecuali untuk bahasa default. Rute kami mengambil bentuk berikut:
Semuanya jatuh pada tempatnya dan kami mengatur lagi tentang fitur baru.
Beberapa saat kemudian, ada kebutuhan untuk menggunakan aplikasi di beberapa domain. Secara umum, situs-situs ini memiliki satu database, tetapi beberapa pengaturan dapat bervariasi tergantung pada domain.
Beberapa situs mungkin multibahasa (apalagi, dengan serangkaian bahasa terbatas, dan tidak dengan semua yang didukung), beberapa - hanya satu bahasa.
Diputuskan untuk memproses semua domain dengan satu aplikasi (nginx proksi semua domain ke satu hulu).
Rangkaian bahasa yang didukung oleh situs tertentu dan bahasa default harus dikonfigurasikan di panel admin, yang merupakan akar dari versi yang diretas dari variabel config / env. Menjadi jelas bahwa solusi saat ini tidak memuaskan Daftar Keinginan kami.
Solusi
Untuk menyederhanakan gambar dan menunjukkan solusinya, saya menggunakan proyek baru pada laravel versi 6.2 dan menolak untuk menggunakan database. Dalam versi 5.x, perbedaannya kecil (tapi tentu saja saya tidak akan mengecatnya).
Kode Proyek Tersedia di GitHub
Pertama, kita perlu menentukan dalam konfigurasi aplikasi semua bahasa yang didukung.
Kami membutuhkan esensi dari situs Site
dan layanan untuk menentukan pengaturan situs.
app / Entities / 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; } }
app / Kontrak / 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; }
app / Layanan / 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' => [
Tambahkan layanan kami ke wadah
app / Penyedia / AppServiceProvider.php <?php namespace App\Providers; use App\Contracts\SiteDetector; use App\Services\SiteDetector\FakeSiteDetector; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider {
Sekarang tentukan rute.
Bagian dari rute yang akan dilokalkan dibingkai oleh minus ganda ( --
). Ini adalah topeng untuk penggantian. Sekarang konfigurasikan masker ini.
config / routes.php <?php return [ 'web.about' => [
Untuk menampilkan komponen pemilihan bahasa, kita perlu mentransfer ke template hanya bahasa-bahasa yang didukung oleh situs. Mari kita menulis middleware untuk ini ...
Http / Middleware / 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); } }
Sekarang Anda perlu menyesuaikan router. Sebaliknya, bukan router itu sendiri, tetapi kumpulan rute ...
app / Custom / Illuminate / Routing / 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']; } }
..., kelas utama aplikasi ...
app / Custom / Illuminate / Foundation / 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()); } }
... dan ganti kelas khusus kami.
Langkah selanjutnya adalah menentukan bahasa dari bagian pertama dari alamat URL. Untuk melakukan ini, sebelum mengirim, kami akan mendapatkan segmen pertamanya, memeriksa dukungan situs untuk bahasa seperti itu, dan mulai mengirim permintaan baru tanpa segmen ini. Mari kita sedikit perbaiki App\Http\Kernel
, dan pada saat yang sama tambahkan App\Http\Middleware\ViewData
middleware kami App\Http\Middleware\ViewData
ke grup web
app / Http / Kernel.php <?php namespace App\Http;
Jika Anda tidak membuat cache rute, maka Anda sudah bisa bekerja. Tetapi dalam pertempuran tanpa cache, idenya bukanlah yang terbaik. Kami telah mengajarkan aplikasi kami untuk menerima rute dari cache, sekarang kami perlu mengajarkan cara menyimpannya dengan benar. Kustomisasi route:cache
perintah konsol 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; } }
Perintah route:clear
hanya menghapus file cache, kami tidak akan menyentuhnya. Tetapi perintah route:list
sekarang tidak mengganggu opsi 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; } }
Sekarang kita perlu menjalankan perintah ini. Sekarang tim vendor akan bekerja. Untuk mengganti implementasi perintah konsol, Anda harus menyertakan penyedia layanan dalam aplikasi yang mengimplementasikan Illuminate\Contracts\Support\DeferrableProvider
. Metode provides()
harus mengembalikan array kunci kontainer yang sesuai dengan kelas-kelas perintah.
app / Penyedia / 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', ]; } }
Dan tentu saja, kami menambahkan penyedia ke konfigurasi.
Sekarang semuanya berfungsi!
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 | +--------+----------+------------------+--------------+-------------------------+------------+
Itu saja. Terima kasih atas perhatian anda!