mcamara / laravel-localization

Easy localization for Laravel
MIT License
3.34k stars 508 forks source link

Support for Laravel Octane #780

Open ju5t opened 3 years ago

ju5t commented 3 years ago

Laravel has recently released a beta version of Laravel Octane.

Because a lot of things are cached, the list of routes doesn't update between requests. This means all prefixed routes do not exist, as Octane didn't make them available. Although Octane is a beta, this caching is unlikely to change.

It would be awesome if Octane support can be added.

edit: See https://github.com/laravel/octane/issues/113 for a workaround/solution. For me this still caused some translation issues, which I will try to demonstrate in more detail later, unless someone beats me to it :)

pmochine commented 3 years ago

@ju5t which kind of translation issues do you get? Just beginning to use laravel-localization for a side project

ju5t commented 3 years ago

@pmochine most details are in https://github.com/laravel/octane/issues/113.

We ended up writing our own simplified localisation support. This was much easier for us. It isn't something we can share at this point in time, as it's not packaged up nicely.

We haven't moved onto Octane though. We're using Forge and you can't switch; you have to migrate.

abishekrsrikaanth commented 3 years ago

can't seem to get this to work on octane, tried what is suggested here https://github.com/laravel/octane/issues/113. No luck

omarherri commented 3 years ago

Did someone managed to resolve this guys ??

mcolominas commented 2 years ago

Hello, I have my own package very similar to this one, and I adapted it to laravel octane, therefore, adapting this package to octane has not been very difficult, here I tell you how to make it work in octane. Routes are always required to be cached.

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Mcamara\LaravelLocalization\LaravelLocalization;

class AppServiceProvider extends ServiceProvider
{
    public function register()
    {
        //Remove singleton
        $this->app->offsetUnset(LaravelLocalization::class);

        //Add bind (Necessary for each cycle to restart)
        $this->app->bind(LaravelLocalization::class, function () {
            return new LaravelLocalization();
        });
    }
}
namespace App\Listeners;

use Mcamara\LaravelLocalization\Facades\LaravelLocalization;

class ReloadRoutes
{
    private static $_last_locale = null;
    /**
     * Handle the event.
     *
     * @param  mixed  $event
     * @return void
     */
    public function handle($event): void
    {
        $locale = LaravelLocalization::setLocale();

        $path = $this->makeLocaleRoutesPath($event->sandbox, $locale);
        if (self::$_last_locale != $locale && file_exists($path) && is_file($path)) {
            self::$_last_locale = $locale;
            include $path;
        }
    }

    /**
     * @param Application $app
     * @param string $locale
     * @return string
     */
    protected function makeLocaleRoutesPath($app, $locale = '')
    {
        $path = $app->getCachedRoutesPath();

        if (!$locale) {
            return $path;
        }

        return substr($path, 0, -4) . '_' . $locale . '.php';
    }
}

In config/octane.php add:

...
'listeners' => [
        ...
        RequestReceived::class => [
            ...Octane::prepareApplicationForNextOperation(),
            ...Octane::prepareApplicationForNextRequest(),
            App\Listeners\ReloadRoutes::class,
        ],
        ...
],
...

With this it should work, but I have to do more tests.

jangaraev commented 2 years ago

Hi gents,

I've also managed to partially make it work with Octane. My way is as per described below:

Since the service provider registers its facade as a singleton we need to switch it to a way proposed by Octane's documentation:

What I did?

  1. disabled an auto-discover for the package in composer.json:
    "extra": {
        "laravel": {
            "dont-discover": [
                "mcamara/laravel-localization"
            ]
        }
    },
  1. created my own service provider for that (to have a better control over this as I'm still didn't make it work 100%)
namespace App\Providers;

use Mcamara\LaravelLocalization\LaravelLocalization;
use Mcamara\LaravelLocalization\LaravelLocalizationServiceProvider;

class LocalizationServiceProvider extends LaravelLocalizationServiceProvider
{
    protected function registerBindings()
    {
        $this->app->bind(LaravelLocalization::class, fn () => new LaravelLocalization());
        $this->app->alias(LaravelLocalization::class, 'laravellocalization');
    }
}
  1. registered it in the appropriate config in app.php
        /*
         * Application Service Providers...
         */
        App\Providers\LocalizationServiceProvider::class,
        ...
        App\Providers\AppServiceProvider::class,
  1. manually registered there as well the facade as its often referenced in blade files
    'aliases' => [
        ...
        'LaravelLocalization' => Mcamara\LaravelLocalization\Facades\LaravelLocalization::class,
    ]
jangaraev commented 2 years ago

I'm still struggling with routes and the homepage without locale chunk doesn't work even with the solution of foreach (LaravelLocalization::getSupportedLocales() ...

Will keep you guys updated if I achieve any meaningful results.

jangaraev commented 2 years ago

Yeah guys, I've managed to make it work completely!

Once setup Laravel Octane via Roadrunner, I discovered several issues with this package:

Here is my solution, it fixes all the issues above:

  1. Add a listener to hook into Octane's RequestReceived event (thanks to @mcolominas):
namespace App\Listeners;

use Illuminate\Foundation\Application;
use Laravel\Octane\Events\RequestReceived;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;

class LoadLocalizedRoutesCache
{
    private static $lastLocale;

    public function handle(RequestReceived $event): void
    {
        // passing request segment is crucial because the package doesn't
        // know the current locale as it was instantiated in service provider

        // there is also an option to don't pass the request segment in case
        // you don't use translatable routes (transRoute() in web.php) in your project
        // in this case the package will correctly resolve the locale and you
        // don't need to pass the 3rd param when binding in service provider
        $locale = LaravelLocalization::setLocale($event->request->segment(1));

        $path = $this->makeLocaleRoutesPath($event->sandbox, $locale);

        if (self::$lastLocale != $locale && is_file($path)) {
            self::$lastLocale = $locale;
            include $path;
        }
    }

    protected function makeLocaleRoutesPath(Application $app, $locale = ''): string
    {
        $path = $app->getCachedRoutesPath();

        if (!$locale) {
            return $path;
        }

        return substr($path, 0, -4) . '_' . $locale . '.php';
    }
}
  1. Reference the listener in Octane's config file:
    'listeners' => [
        RequestReceived::class => [
            ...Octane::prepareApplicationForNextOperation(),
            ...Octane::prepareApplicationForNextRequest(),
            \App\Listeners\LoadLocalizedRoutesCache::class
        ],
  1. disabled an auto-discover for the package in composer.json:
    "extra": {
        "laravel": {
            "dont-discover": [
                "mcamara/laravel-localization"
            ]
        }
    },
  1. created my own service provider instead of disabled native one in composer.json:
namespace App\Providers;

use Mcamara\LaravelLocalization\LaravelLocalization;
use Mcamara\LaravelLocalization\LaravelLocalizationServiceProvider;

class LocalizationServiceProvider extends LaravelLocalizationServiceProvider
{
    protected function registerBindings()
    {
        $fn = fn () => new LaravelLocalization();

        // the conditional check below is important
        // when you do caching routes via `php artisan route:trans:cache` if binding
        // via `bind` used you will get incorrect serialized translated routes in cache
        // files and that's why you'll get broken translatable route URLs in UI

        // again, if you don't use translatable routes, you may get rid of this check
        // and leave only 'bind()' here

        // the 3rd parameter is important to be passed to 'bind'
        // otherwise the package's instance will be instantiated every time
        // you reference it and it won't get proper data for 'serialized translatable routes'
        // class variable, this will make impossible to use translatable routes properly
        // but oveall the package will still work stable except generating the same URLs
        // for translatable routes independently of locale

        if ($this->runningInOctane()) {
            $this->app->bind(LaravelLocalization::class, $fn, true);
        } else {
            $this->app->singleton(LaravelLocalization::class, $fn);
        }

        $this->app->alias(LaravelLocalization::class, 'laravellocalization');
    }

    private function runningInOctane(): bool
    {
        return !$this->app->runningInConsole() && env('LARAVEL_OCTANE');
    }
}
  1. registered it in the appropriate config in app.php
        /*
         * Application Service Providers...
         */
        App\Providers\LocalizationServiceProvider::class,
        ...
        App\Providers\AppServiceProvider::class,
  1. manually registered there as well the facade as its often referenced in blade files:
    'aliases' => [
        ...
        'LaravelLocalization' => Mcamara\LaravelLocalization\Facades\LaravelLocalization::class,
    ]

My project is a typical Laravel project:

We use several translatable routes in the project. Most of problems get from there. Basically adapting the package appeared not so hard, but making translatable routes work made me a bit nervous :-)

Notes:

  1. You don't need to touch your routes at all, no need to foreach (...) over locales as described in posts above.
  2. It's important to use routes cache there (php artisan route:trans:cache)
jangaraev commented 2 years ago

@mcamara , can I prepare a PR with those changes?

mcamara commented 2 years ago

@Jangaraev please go ahead, we will really appreciate it

taai commented 2 years ago

can I prepare a PR with those changes?

@jangaraev Are you still planning to make a PR?

ilyasozkurt commented 2 years ago

@jangaraev you've saved my day dude. Thank you so much for the great solution :)

jangaraev commented 2 years ago

@jangaraev Are you still planning to make a PR?

sorry, was sick for a long time. yep, in a week will try to propose something there.

parallels999 commented 1 year ago

@jangaraev please use markdown correctly for highlight ```php, thanks

korridor commented 1 year ago

Is anybody using one of the workarounds described in this issue and everything still works? I just tested both and I had problems with all of them. Just wondering if I did something wrong or if this workaround just don't work anymore.

vic-pic commented 3 months ago

Hi @jangaraev, I am trying your workaround, but it doesn't work. The octane route cache file for the selected locale was not created, so the listener doesn't include it and octane returns 404.

I'm using Laravel 10 and PHP 8.2

How can I fix this issue?

Thanks, Vincenzo

korridor commented 3 months ago

@vic-pic I already wrote in the PR #833, but not in here maybe this helps you as well:

I tried the solution and it didn't solve the problem for me. I also looked into the code of the solution, and it seems a bit hacky, but I'm not that deep into this plugin.

Since this broke my production environment and I needed a multi-language solution fast, I uninstalled this plugin and used this instead: https://github.com/codezero-be/laravel-localized-routes

Worked with Octane out of the box and the migration process was surprisingly easy. I think it is also nice that route:cache works normally there.

ilyasozkurt commented 3 months ago

Hello guys,

Here's my workaround. First of all you need to understand sandbox is an isolated instance of your app. Whenever you have any request one of these instances are getting handle the request.

So, it means that you need to load localed routes whenever you have request. Because you do not know which sandboxed instance of your app is going to handle "the" request and what language is currently adjusted for that isolated instance.

Here's my LoadLocalizedRoutesCache


namespace App\Listeners;

use Barryvdh\Debugbar\LaravelDebugbar;
use Illuminate\Foundation\Application;
use Laravel\Octane\Events\RequestReceived;
use Mcamara\LaravelLocalization\Facades\LaravelLocalization;

class LoadLocalizedRoutesCache
{

    /**
     * Handle request received event
     *
     * @param RequestReceived $event
     * @return void
     */
    public function handle(RequestReceived $event): void
    {

        // Fix routes
        $this->fixRoutes($event);

        // Fix debugbar
        $this->fixDebugbar($event->sandbox);

    }

    /**
     * Create the path to the locale routes file
     *
     * @param Application $sandbox
     * @param string $locale
     * @return string
     */
    protected function makeLocaleRoutesPath(Application $sandbox, string $locale = 'en'): string
    {

        // Get the path to the cached routes file
        $path = $sandbox->getCachedRoutesPath();

        // Return the path to the locale routes file
        return substr($path, 0, -4) . '_' . $locale . '.php';

    }

    /**
     * @param Application $sandbox
     * @return void
     */
    protected function fixDebugbar(Application $sandbox): void
    {

        $sandbox->singleton(LaravelDebugbar::class, function ($sandbox) {
            return new LaravelDebugbar($sandbox);
        });

    }

    /**
     * @param RequestReceived $event
     * @return void
     */
    protected function fixRoutes(RequestReceived $event): void
    {

        // Get the sandbox
        $sandbox = $event->sandbox;

        // Get default locale
        $defaultLocale = LaravelLocalization::getDefaultLocale();

        // Get the locale code from the request
        $currentCode = $event->request->segment(1, $defaultLocale);

        // Set the locale
        $sandbox->setLocale($currentCode);
        LaravelLocalization::setLocale($currentCode);

        // Get the path to the locale routes file
        $path = $this->makeLocaleRoutesPath($sandbox, $currentCode);

        // If the locale the file exists, include it
        if (is_file($path)) {
            include $path;
        }

        // Fix home route not found, this is required for auto redirection
        $sandbox['router']->addRoute('GET', '/', function () use ($currentCode) {
            return redirect(LaravelLocalization::localizeUrl('/', $currentCode));
        });

    }

}

You need to add this event handle to octane.php like below.

image

I hope this helps to you too.