Введение
Laravel - сам по себе классный фреймворк PHP. У него есть свои плюсы и минусы. У меня в компании используется laravel почти на всех проектах компании. В большинстве случаях в качестве административной панели используется laravel nova. И всё бы ничего если бы на одном из проектов, заказчик не захотел локализацию всей админки для модераторов из разных стран, с возможностью добавления языков.
Основные проблемы
Задавшись этой сложной задачей передо мной встало несколько проблем:
Добавления языков
Хранения данных полей админ панели
Переключатель языков
И последняя проблема появившиеся в конце всего процесса - СЕССИИ Laravel nova.
Добавление языков
Самое простое в локализации laravel nova это хранение языков. Так мне необходимо было создать хранилище языков с возможностью добавления. Я создал таблицу для хранения и ресурс nova.
public function up()
{
Schema::create('languages', function (Blueprint $table) {
$table->id();
$table->string('key_lang', 255);
$table->string('title', 255);
$table->timestamps();
});
}
//миграция таблицы языков
public function fields(Request $request)
{
return [
ID::make(__('ID'), 'id')->sortable(),
Text::make(__("LanguageKey"), 'key_lang')
->rules('required')
->sortable(),
Text::make(__("Title"), 'title')
->rules('required')
->sortable()
];
}
//ресурс nova языков
Хранения данных полей админ панели
Так все основные поля nova хранятся в директории resources/lang/vendor/nova в json файлах имеющих в качестве названия ключ языка. Мне пришла идея создать новую таблицу для хранения данных локализации и создать observer который будет брать существующий файл json laravel и дополнять его информацией при сохранении с таблицы.
Для того чтобы хранить данные и в БД, я использовал json поле. Таким образом миграция приняла следующий вид:
public function up()
{
Schema::create('admin_panels', function (Blueprint $table) {
$table->id();
$table->foreignId('key_lang_id')->unique()->constrained('languages');
$table->json('content');
$table->timestamps();
});
}
//таблица хранения полей для административной панели
Для того чтобы для каждого поля не писать в ресурсе для каждого необходимого поля свое поле для отображения, я решил собрать все необходимые мне ключи локализации в константу массива, с генерировать текстовые поля в массиве и поместить их в поле armincms/json (https://github.com/armincms/json).
protected static $adminField = [
'LoggingProfileChanges', 'ChangedByWhom', 'ListOfChanges' ...... // Все ключи локализации
];
public function fields(Request $request)
{
foreach (self::$adminField as $field) {
$res[] = Text::make(__($field), $field)
->rules('required')
->hideFromIndex();
}
$result = [
ID::make(__('ID'), 'id')->sortable(),
BelongsTo::make(__("LanguageKey"), 'key_lang', Language::class)
->searchable(true)
->creationRules('unique:admin_panels,key_lang_id')
->updateRules('unique:admin_panels,key_lang_id,{{resourceId}}')
->sortable(),
Json::make("content", $res)
];
return $result;
}
После того как всё это проделано оставалось настроить сохранение в json файлы nova используя observer. Написав рекурсивную замену значений json файла я добился успеха.
public function created(AdminPanel $adminPanel)
{
$data = $adminPanel->getAttributes();
$lang = Language::find($data['key_lang_id']);
$path = resource_path('/lang/vendor/nova/'.$lang->key_lang.'.json');
if (file_exists($path)) {
$file = file_get_contents($path);
$original = json_decode($file, True);
$original = array_replace_recursive($original, json_decode($data['content'], True));
$handle = fopen($path, 'w+');
fputs($handle, json_encode($original));
fclose($handle);
} else {
$handle = fopen($path, 'w+');
fputs($handle, $data['content']);
fclose($handle);
}
}
public function updated(AdminPanel $adminPanel)
{
$idLang = $adminPanel->getOriginal('key_lang_id');
$data = $adminPanel->getChanges();
if (isset($data['key_lang_id'])) {
$lang = Language::find($data['key_lang_id']);
} else {
$lang = Language::find($idLang);
}
$path = resource_path('/lang/vendor/nova/'.$lang->key_lang.'.json');
$file = file_get_contents($path);
$original = json_decode($file, True);
$original = array_replace_recursive($original, json_decode($data['content'], True));
$handle = fopen($path, 'w+');
fputs($handle, json_encode($original));
fclose($handle);
}
Сессии и переключатель языков
Подключение переключателя языков по началу казалось лёгкой задачей так, как я думал можно взять готовый переключаетель с laravel nova packages, но я ошибался. В большестве случаев, либо они не работали на текущей версии laravel nova(v3.23.2 )
, либо сталкивался с проблемой того что языки брались из конфига, и даже при попытке добавить туда язык нужно было чистить кэш, что мне вовсе не подходило так как всё должно происходить автоматически.
В конечном итиоге пришёл к выводу что нужно делать свой переключатель. Сделав его и залив на сервер, появилась главная проблема - отсутсвие сессий в laravel nova. И тогда я уже думал что это провал. До демо перед заказкчиком было 3 дня. Пытаясь найти решение на протяжении всего дня, пришел к выводу что нужно как то прикручивать сессии вручную. Для меня это было сложной задачей так опыта в написании на laravel меньше пол года.
Посидев над проблемой два дня мне всё таки удалось прикрутить сессии и настроить шаблон resources/views/vendor/nova/partials/user.blade.php
user.blade.php
<dropdown-trigger class="h-9 flex items-center">
@isset($user->email)
<img
src="https://secure.gravatar.com/avatar/{{ md5(\Illuminate\Support\Str::lower($user->email)) }}?size=512"
class="rounded-full w-8 h-8 mr-3"
/>
@endisset
<span class="text-90">
{{ $user->name ?? $user->email ?? __('Nova User') }}
</span>
</dropdown-trigger>
<dropdown-menu slot="menu" width="200" direction="rtl">
<ul class="list-reset">
@foreach (App\Http\Controllers\LanguageController::getLangs() as $lang => $language)
@if ($lang != Illuminate\Support\Facades\App::getLocale())
<li>
<a class="block no-underline text-90 hover:bg-30 p-3" href="{{ route('lang.switch', $lang) }}"> {{$language}}</a>
</li>
@endif
@endforeach
</ul>
<ul class="list-reset">
<li>
<a href="{{ route('nova.logout') }}" class="block no-underline text-90 hover:bg-30 p-3">
{{ __('Logout') }}
</a>
</li>
</ul>
</dropdown-menu>
LanguageController.php
public function switchLang($lang)
{
if (array_key_exists($lang, self::getLangs())) {
Session::put('applocale', $lang);
}
return Redirect::back();
}
public static function getLangs()
{
$result = [];
$allLang = Language::all();
foreach ($allLang as $lang) {
$result[$lang->key_lang] = $lang->title;
}
return $result;
}
LanguageMiddleware.php
public function handle($request, Closure $next)
{
if (Session()->has('applocale') AND array_key_exists(Session()->get('applocale'), LanguageController::getLangs())) {
App::setLocale(Session()->get('applocale'));
}
else {
App::setLocale(config('app.fallback_locale'));
}
return $next($request);
}
В конце нужно добавить класс в Kernel:
protected $middlewareGroups = [
'web' => [
...
\App\Http\Middleware\Language::class,
],
...
];
Так же можно это использовать заменив получение языков из контроллера, на получение языков из конфига.
Итог
В конечном результате у меня вышел функционал, с добавлением языков и переводов в административную панель и переключением языков админки с сессиями.