
Hallo habr
UPD
In dieser Ressource ist die Relevanz des Artikels möglicherweise null mal ein Kommentar . Die im Artikel beschriebene Aufgabe kann durch die mcamara / laravel-Lokalisierungsbibliothek mit weniger Schmerzen gelöst werden.
Danke für den Tipp DExploN !
Kat hob. Mit Null multipliziert ist der Boden.
Ich möchte Ihnen erzählen, wie in einem Projekt ein Problem mit dem Routing aufgetreten ist und wie wir es gelöst haben.
Zunächst war unser Projekt die am häufigsten verwendete Site. Die Website entwickelte sich, das Publikum expandierte und es entstand das Bedürfnis, die Mehrsprachigkeit zu unterstützen. Das Projekt basierte auf dem Laravel-Framework und es gab keine Probleme mit der Mehrsprachigkeit (die gewünschte Sprache wurde aus der Sitzung entfernt oder die Standardsprache wurde verwendet). Wir haben Übersetzungen geschrieben, Übersetzungsschlüssel anstelle von hartcodierten Phrasen verschrieben und die folgenden Funktionen verwendet, um zu funktionieren.
Das problem
Irgendwann stellte das SEO-Team fest, dass dieser Ansatz das Ranking der Website beeinträchtigt. Anschließend erhielt das Entwicklungsteam den Befehl, der URL Sprachunterordner mit Ausnahme der Standardsprache hinzuzufügen. Unsere Routen hatten folgende Form:
Alles passte zusammen und wir machten uns wieder an die neuen Funktionen.
Wenig später musste die Anwendung auf mehreren Domänen bereitgestellt werden. Im Allgemeinen verfügen diese Sites über eine Datenbank, einige Einstellungen können jedoch je nach Domäne variieren.
Einige Websites sind möglicherweise mehrsprachig (außerdem mit einer begrenzten Anzahl von Sprachen und nicht mit allen unterstützten Sprachen), andere nur in einer Sprache.
Es wurde beschlossen, alle Domänen mit einer Anwendung zu verarbeiten (nginx überträgt alle Domänen auf eine vorgelagerte).
Die von einer bestimmten Site unterstützten Sprachen und die Standardsprache sollten im Admin-Bereich konfiguriert werden, der das Stammverzeichnis der gehackten Version der config / env-Variablen ist. Es stellte sich heraus, dass die aktuelle Lösung unseren Wunschzettel nicht erfüllt.
Lösung
Um das Bild zu vereinfachen und die Lösung zu demonstrieren, habe ich ein neues Projekt auf laravel Version 6.2 implementiert und mich geweigert, die Datenbank zu verwenden. In den Versionen 5.x sind die Unterschiede gering (aber ich werde sie natürlich nicht malen).
Projektcode auf GitHub verfügbar
Zunächst müssen in der Anwendungskonfiguration alle unterstützten Sprachen angegeben werden.
Wir benötigen das Wesentliche der Site
Site und den Dienst zum Bestimmen der Site-Einstellungen.
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 / Verträge / 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 / Services / 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' => [
Fügen Sie unseren Service dem Container hinzu
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 {
Definieren Sie nun die Routen.
Teile von Routen, die lokalisiert werden sollen, sind durch doppelte Minuszeichen ( --
) eingerahmt. Dies sind Masken zum Ersetzen. Konfigurieren Sie nun diese Masken.
config / routes.php <?php return [ 'web.about' => [
Um die Sprachauswahlkomponente anzuzeigen, müssen nur die von der Site unterstützten Sprachen in die Vorlage übertragen werden. Lassen Sie uns eine Middleware dafür schreiben ...
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); } }
Jetzt müssen Sie den Router anpassen. Eher nicht der Router selbst, sondern eine Sammlung von Routen ...
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']; } }
..., die Hauptklasse der Anwendung ...
app / Benutzerdefiniert / 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()); } }
... und ersetzen Sie unsere benutzerdefinierten Klassen.
Der nächste Schritt besteht darin, die Sprache aus dem ersten Teil der URL-Adresse zu bestimmen. Dazu rufen wir vor dem Versand das erste Segment ab, überprüfen die Unterstützung der Site für eine solche Sprache und beginnen mit dem Versand einer neuen Anfrage ohne dieses Segment. App\Http\Middleware\ViewData
wir die Klasse App\Http\Kernel
und fügen gleichzeitig unsere Middleware App\Http\Middleware\ViewData
zur App\Http\Middleware\ViewData
app / Http / Kernel.php <?php namespace App\Http;
Wenn Sie die Routen nicht zwischenspeichern, können Sie bereits arbeiten. Aber in einer Schlacht ohne Cache ist die Idee nicht die beste. Wir haben unserer Anwendung bereits beigebracht, Routen aus dem Cache zu empfangen. Jetzt müssen wir lernen, wie sie richtig gespeichert wird. Passen Sie den Konsolenbefehl route:cache
app / Benutzerdefiniert / 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; } }
Der Befehl route:clear
löscht einfach die Cache-Datei, wir werden sie nicht berühren. Der Befehl route:list
beeinträchtigt jetzt jedoch nicht die Option locale
.
app / Benutzerdefiniert / 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; } }
Jetzt müssen wir diese Befehle zum Laufen bringen. Jetzt werden Verkäuferteams arbeiten. Um die Implementierung von Konsolenbefehlen zu ersetzen, müssen Sie einen Dienstanbieter in die Anwendung aufnehmen, der die Illuminate\Contracts\Support\DeferrableProvider
. Die provides()
-Methode sollte ein Array von Containerschlüsseln zurückgeben, die den Befehlsklassen entsprechen.
app / Providers / 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', ]; } }
Und natürlich fügen wir den Provider der Konfiguration hinzu.
Jetzt funktioniert alles!
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 | +--------+----------+------------------+--------------+-------------------------+------------+
Das ist alles Vielen Dank für Ihre Aufmerksamkeit!