I am looking for a working solution, to translate queued emails in laravel-5.
Unfortunately, all emails use the default locale (defined under app.locale).
Let's assume, we have two emails in the pipeline, one for an English en user and another for an Japanese jp user.
What data should I pass to the Mail facade to translate (localize) the queued emails?
// User model
$user = User:find(1)->first();
Mailer::queue($email, 'Party at Batman\'s cave (Batcave)', 'emails.party-invitation', [
...
'locale' => $user->getLocale(), // value: "jp", but does not work
'lang' => $user->getLocale(), // value: "jp", but does not work
'language' => $user->getLocale(), // value: "jp", but does not work
]);
I have been struggling to get this done in a more efficient way. Currently I have it set up like this. Hopefully this helps someone in the future with this issue:
// Fetch the locale of the receiver.
$user = Auth::user();
$locale = $user->locale;
Mail::queue('emails.welcome.template', ['user' => $user, 'locale' => $locale], function($mail) use ($user, $locale) {
$mail->to($user->email);
$mail->subject(
trans(
'mails.subject_welcome',
[], null, $locale
)
);
});
And use the following in your template:
{{ trans('mails.welcome', ['name' => ucfirst($user['first_name'])], null, $locale) }}
Note: do not forget to restart your queue
If your emails inherits the built-in Illuminate\Mail\Mailable class you can build your own Mailable class that will take care of translations.
Create your own SendQueuedMailable class that inherits from Illuminate\Mail\SendQueuedMailable.
Class constructor will take current app.location from config and remember it in a property (because laravel queues serializes it).
In queue worker it will take the property back from config and set the setting to the current environment.
<?php
namespace App\Mail;
use Illuminate\Contracts\Mail\Mailer as MailerContract;
use Illuminate\Contracts\Mail\Mailable as MailableContract;
use Illuminate\Mail\SendQueuedMailable as IlluminateSendQueuedMailable;
class SendQueuedMailable extends IlluminateSendQueuedMailable
{
protected $locale;
public function __construct(MailableContract $mailable)
{
parent::__construct($mailable);
$this->locale = config('app.locale');
}
public function handle(MailerContract $mailer)
{
config(['app.locale' => $this->locale]);
app('translator')->setLocale($this->locale);
parent::handle($mailer);
}
}
Then, create your own Mail class that inherits from Illuminate\Mail\Mailable
<?php
namespace App\Mail;
use Illuminate\Contracts\Queue\Factory as Queue;
use Illuminate\Mail\Mailable as IlluminateMailable;
class Mailable extends IlluminateMailable
{
public function queue(Queue $queue)
{
$connection = property_exists($this, 'connection') ? $this->connection : null;
$queueName = property_exists($this, 'queue') ? $this->queue : null;
return $queue->connection($connection)->pushOn(
$queueName ?: null, new SendQueuedMailable($this)
);
}
}
And, voila, all your queued mailables inherited from App\Mailable class automatically will take care of current locale.
In Laravel 5.6 is a locale function added to Mailable:
$infoMail->locale('jp');
Mail::queue($infoMail);
Laravel 5.7.7 introduced the HasLocalePreference interface to solve this issue.
class User extends Model implements HasLocalePreference
{
public function preferredLocale() { return $this->locale; }
}
Now if you use Mail::to() function, Laravel will send with the correct locale.
Also introduced was the Mail chainable locale() function.
Mail::to($address)->locale($locale)->send(new Email());
Here's a solution that worked for me. In my example, I am processing the booking on a queue, using a dedicated Job class.
When you dispatch a job on a queue, pass the locale, you want your email in. For example:
ProcessBooking::dispatch($booking, \App::getLocale());
Then, in your job class (ProcessBooking in my example) store the locale in a property:
protected $booking;
protected $locale;
public function __construct(Booking $booking, $locale)
{
$this->booking = $booking;
$this->locale = $locale;
}
And in your handle method temporarily switch locales:
public function handle()
{
// store the locale the queue is currently running in
$previousLocale = \App::getLocale();
// change the locale to the one you need for job to run in
\App::setLocale($this->locale);
// Do the stuff you need, e.g. send an email
Mail::to($this->booking->customer->email)->send(new NewBooking($this->booking));
// go back to the original locale
\App::setLocale($previousLocale);
}
Why do we need to do this?
Queued emails get the default locale since queue workers are separate Laravel apps. If you ever ran into a problem, when you cleared your cache, but queued 'stuff' (e.g. emails) still showed the old content, it's because of that. Here's an explanation from Laravel docs:
Remember, queue workers are long-lived processes and store the booted application state in memory. As a result, they will not notice changes in your code base after they have been started. So, during your deployment process, be sure to restart your queue workers.
using preferredLocale() is the "official" way to go.
you can implement HasLocalePreference and add method preferredLocale() to the notifiable model (as #Peter-M suggested):
use Illuminate\Contracts\Translation\HasLocalePreference;
class User extends Model implements HasLocalePreference
{
/**
* Get the user's preferred locale.
*
* #return string
*/
public function preferredLocale()
{
return $this->locale;
}
}
and then you just send the notification to that model and locale will be automatically applied to your email template
$user->notify(new InvoicePaid($invoice));
This also works with queued mail notifications.
read more here:
https://floyk.com/en/post/how-to-change-language-for-laravel-email-notifications
I'm faced with the same problem
Without extending the queue class the quickest / dirtiest solution would be to create an email template for each locale.
Then when you create the queue, select the local template i.e.
Mail::queue('emails.'.App::getLocale().'notification', function($message)
{
$message->to($emails)->subject('hello');
});
If the local is set to IT (italian) This will load the view emails/itnotification.blade.php
As I said... Dirty!
Since this method is actually horrible I came across this answer
And its working for me, you actually send the entire translated html version of the email in a variable to the queue and have a blank blade file that echos out the variable when the queue runs.
Related
I have two different types of theme/css for my emails that are sent from my app. A default one for system emails (reset password etc) and one for consumer emails (emails as a result of actions form users within the app).
In my consumer emails, in the toMail() method of the mailable/notification I execute the mailable like so:
public function toMail($notifiable)
{
return (new MailMessage)
->theme($this->theme)
->from($this->booking->school->school_email, $this->booking->school->name)
->subject($this->getSubject())
->attachData($this->booking->payments()->first()->toPdf()->output(), 'Invoice.pdf')
->markdown('mail.notifications.bookings.paid_booking', $this->toArray());
}
Notice that I call ->theme(...) on the mailable. This works perfectly fine and the correct theme is set in the template that is received in the mailbox.
When I try to use Laravel's Mailable Preview within a route:
Route::get('/mail/resetpass', function () {
return (new App\Notifications\ResetPasswordNotification('token'))->toMail(\App\User::find(2));
});
Route::get('/mail/reserved', function () {
$booking = \App\Domains\Customers\Models\Booking::find(1);
return (new \App\Domains\Customers\Notifications\ReservedBookingConfirmation($booking))->toMail($booking->customer);
});
The "default" theme, as defined in my config files is the one that is used, and my call to ->theme(...) is ignored.
Is there a solution for this? Changing the config value, isn't a feasible option as I actually want to use this functionality to allow my users to view their emails in the browser. I'm unsure what else to try.
The issue lies in the render() method of the Illuminate\Notifications\Messages\MailMessage class.
This is fixed in Laravel 8 and so this solution only applies if your project is using laravel 7 or below
it does not call the theme. I extended the class and have overridden the render method. Adding the call to ->theme(...) like so.
<?php
namespace App\Mail;
use Illuminate\Container\Container;
use Illuminate\Mail\Markdown;
use Illuminate\Notifications\Messages\MailMessage as Original;
class MailMessage extends Original
{
/**
* Render the mail notification message into an HTML string.
*
* #return string
*/
public function render()
{
if (isset($this->view)) {
return Container::getInstance()->make('mailer')->render(
$this->view, $this->data()
);
}
return Container::getInstance()
->make(Markdown::class)
->theme($this->theme ?? config('mail.markdown.theme')) //add this line here
->render($this->markdown, $this->data());
}
}
I'm writing a small API in Laravel, partly for the purposes of learning this framework. I think I have spotted a gaping hole in the docs, but it may be due to my not understanding the "Laravel way" to do what I want.
I am writing an HTTP API to, amongst other things, list, create and delete system users on a Linux server. The structure is like so:
Routes to /v1/users connect GET, POST and DELETE verbs to controller methods get, create and delete respectively.
The controller App\Http\Controllers\UserController does not actually run system calls, that is done by a service App\Services\Users.
The service is created by a ServiceProvider App\Providers\Server\Users that registers a singleton of the service on a deferred basis.
The service is instantiated by Laravel automatically and auto-injected into the controller's constructor.
OK, so this all works. I have also written some test code, like so:
public function testGetUsers()
{
$response = $this->json('GET', '/v1/users');
/* #var $response \Illuminate\Http\JsonResponse */
$response
->assertStatus(200)
->assertJson(['ok' => true, ]);
}
This also works fine. However, this uses the normal bindings for the UserService, and I want to put a dummy/mock in here instead.
I think I need to change my UserService to an interface, which is easy, but I am not sure how to tell the underlying test system that I want it to run my controller, but with a non-standard service. I see App::bind() cropping up in Stack Overflow answers when researching this, but App is not automatically in scope in artisan-generated tests, so it feels like clutching at straws.
How can I instantiate a dummy service and then send it to Laravel when testing, so it does not use the standard ServiceProvider instead?
The obvious way is to re-bind the implementation in setUp().
Make your self a new UserTestCase (or edit the one provided by Laravel) and add:
abstract class TestCase extends BaseTestCase
{
use CreatesApplication;
protected function setUp()
{
parent::setUp();
app()->bind(YourService::class, function() { // not a service provider but the target of service provider
return new YourFakeService();
});
}
}
class YourFakeService {} // I personally keep fakes in the test files itself if they are short
Register providers conditionally based on environment (put this in AppServiceProvider.php or any other provider that you designate for this task - ConditionalLoaderServiceProvider.php or whatever) in register() method
if (app()->environment('testing')) {
app()->register(FakeUserProvider::class);
} else {
app()->register(UserProvider::class);
}
Note: drawback is that list of providers is on two places one in config/app.php and one in the AppServiceProvider.php
Aha, I have found a temporary solution. I'll post it here, and then explain how it can be improved.
<?php
namespace Tests\Feature;
use Tests\TestCase;
use \App\Services\Users as UsersService;
class UsersTest extends TestCase
{
/**
* Checks the listing of users
*
* #return void
*/
public function testGetUsers()
{
$this->app->bind(UsersService::class, function() {
return new UsersDummy();
});
$response = $this->json('GET', '/v1/users');
$response
->assertStatus(200)
->assertJson(['ok' => true, ]);
}
}
class UsersDummy extends UsersService
{
public function listUsers()
{
return ['tom', 'dick', 'harry', ];
}
}
This injects a DI binding so that the default ServiceProvider does not need to kick in. If I add some debug code to $response like so:
/* #var $response \Illuminate\Http\JsonResponse */
print_r($response->getData(true));
then I get this output:
Array
(
[ok] => 1
[users] => Array
(
[0] => tom
[1] => dick
[2] => harry
)
)
This has allowed me to create a test with a boundary drawn around the PHP, and no calls are made to the test box to interact with the user system.
I will next investigate whether my controller's constructor can be changed from a concrete implementation hint (\App\Services\Users) to an interface, so that my test implementation does not need to extend from the real one.
I have the following definition:
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\SomeClass;
class SomeProvider extends ServiceProvider
{
protected $defer = true;
public function register()
{
$this->app->bind(SomeClass::class, function ($app)
{
return new SomeClass();
});
}
public function provides()
{
die("This never gets called");
return [SomeClass::class];
}
}
And it returns an instance of SomeClass as expected, except that according to the documentation, if $defer is true then the provides() method should be called. No matter what I set $defer to, and no matter if I actually ask for an instance of SomeClass or not, provides() is never called.
The way I'm asking for an instance of the class is as follows:
App::make('SomeClass');
Short answer:
Your compiled manifest file is already compiled by framework.
On the first time when Laravel build the application (and resolves all of services providers in IoC container)
it writes to cached file named services.php (that is, the manifest file, placed in: bootstrap/cache/services.php).
So, if you clear the compiled via php artisan clear-compiled command it should force framework to rebuild the manifest file and you could to note that provides method is called.
On the next calls/requests provides method is not called anymore.
The sequence of framework boot is nearly like this:
//public/index.php
$app = new Illuminate\Foundation\Application(
realpath(__DIR__.'/../')
);
$kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
\Illuminate\Foundation\Http\Kernel::__construct();
\Illuminate\Foundation\Http\Kernel::handle();
\Illuminate\Foundation\Http\Kernel::sendRequestThroughRouter();
\Illuminate\Foundation\Http\Kernel::bootstrap();
\Illuminate\Foundation\Application::bootstrapWith();
# where $bootstrapper is a item from \Illuminate\Foundation\Http\Kernel::$bootstrappers
# and $this is instance of \Illuminate\Foundation\Application
\Illuminate\Foundation\Application::make($bootstrapper)->bootstrap($this);
One of bootstrappers is Illuminate\Foundation\Bootstrap\RegisterProviders which
invokes \Illuminate\Foundation\Application::registerConfiguredProviders() and then invokes
\Illuminate\Foundation\ProviderRepository::__construct() and finally:
\Illuminate\Foundation\ProviderRepository::load()
When \Illuminate\Foundation\ProviderRepository::load() is called all services providers is registered and
\Illuminate\Support\ServiceProvider::provides() are called also well.
And here is the snippet you should know (from \Illuminate\Foundation\ProviderRepository::load):
/**
* Register the application service providers.
*
* #param array $providers
* #return void
*/
public function load(array $providers)
{
$manifest = $this->loadManifest();
// First we will load the service manifest, which contains information on all
// service providers registered with the application and which services it
// provides. This is used to know which services are "deferred" loaders.
if ($this->shouldRecompile($manifest, $providers)) {
$manifest = $this->compileManifest($providers);
}
// Next, we will register events to load the providers for each of the events
// that it has requested. This allows the service provider to defer itself
// while still getting automatically loaded when a certain event occurs.
foreach ($manifest['when'] as $provider => $events) {
$this->registerLoadEvents($provider, $events);
}
// We will go ahead and register all of the eagerly loaded providers with the
// application so their services can be registered with the application as
// a provided service. Then we will set the deferred service list on it.
foreach ($manifest['eager'] as $provider) {
$this->app->register($this->createProvider($provider));
}
$this->app->addDeferredServices($manifest['deferred']);
}
\Illuminate\Foundation\ProviderRepository::compileManifest() is the place where your provides() method is performed.
After doing my own testing, it would seem this is an issue (maybe "issue" is a strong word in this case).
If you register something in a service provider which has the name of a class, Laravel is just going to return that class and disregard whatever is in the service provider. I started doing the same thing as you did....
protected $defer = true;
public function register()
{
$this->app->bind(SomeClass::class, function ($app)
{
return new SomeClass();
});
}
public function provides()
{
dd('testerino');
}
$test = \App::make('App\SomeClass');
And $test is an instance of SomeClass. However if I make the following change...
$this->app->bind('test', function ($app) { ... }
And use
$test = \App::make('test');
Then it hits the deffered function and outputs the text testerino.
I think the issue here is that Laravel knows you are just trying to grab a class. In this instance, there is no reason to register what you are trying to register with the container, you aren't doing anything except telling Laravel to make an instance of App\SomeClass when it should make an instance of App\SomeClass.
However, if you tell Laravel you want an instance of App\SomeClass when you call App::make('test'), then it actually needs to bind that class to test so then I think it starts to pay attention to the service provider.
I need to use a custom Implementation of UrlGenerator. So how can I change the default binding of laravel, that is implemented somewhere deep in the core as
'url' => ['Illuminate\Routing\UrlGenerator', 'Illuminate\Contracts\Routing\UrlGenerator'],
against my own implementation?
Furthermore I am not shure. I assume this line above does actually two things. it will store the bindinung under the key "url" and it will also do the mapping of the Interface to the class. So I actually need to override both! How to do that? Furthemore how to find out if this must be bound as "shared"(singleton) or "new instance every time"?
Thanks very much!
Take a look at the Service Container guide http://laravel.com/docs/5.1/container
In this specific case I think all you need to do is to tell the app to replace the alias that already exists.
To do that I would recommend creating a ServiceProvider, registering int the config/app.php file and inside that one in the register method put something like:
$this->app->bind('Illuminate\Routing\UrlGenerator', 'yourownclasshere');
Let us know if it works.
Update: I removed the option that didn't work and left only the one that worked.
I did what Nestor said in his answer, but it didn't quite work for me. So this is what I did to make it work.
Inside my service provider in method register I first tried this:
$this->app->bind('url', MyCustomProvider::class);
This did register my URL provider instead of the default one. The problem was that now my provider didn't have any access to routes. I checked the Laravel code for \Illuminate\Routing\RoutingServiceProvider because it has a method registerUrlGenerator for registering the URL provider. This method did a direct instantiation of the Laravel URL generator Illuminate\Routing\UrlGenerator and giving proper parameters in the constructor.
So, I did the same in my service provider. Instead of doing $this->app->bind I did $this->app->singleton('url', function ($app) { ... }) and provided basically the same code in the closure function as in RoutingServiceProvider::registerUrlGenerator but created the instance of my URL generator. This then worked properly, and my generator is now called every time. The final code was this:
// the code is copied from the \Illuminate\Routing\RoutingServiceProvider::registerUrlGenerator() method
$this->app->singleton('url', function ($app) {
/** #var \Illuminate\Foundation\Application $app */
$routes = $app['router']->getRoutes();
$app->instance('routes', $routes);
// *** THIS IS THE MAIN DIFFERENCE ***
$url = new \My\Specific\UrlGenerator(
$routes,
$app->rebinding(
'request',
static function ($app, $request) {
$app['url']->setRequest($request);
}
),
$app['config']['app.asset_url']
);
$url->setSessionResolver(function () {
return $this->app['session'] ?? null;
});
$url->setKeyResolver(function () {
return $this->app->make('config')->get('app.key');
});
$app->rebinding('routes', static function ($app, $routes) {
$app['url']->setRoutes($routes);
});
return $url;
});
I hate copying the code, so it seems to me that the problem is in the base implementation. It should take the correct contract for URL generator instead of making direct instantiation of a base class.
I tried the Kosta's approach but it didn't fully work for me because it somehow created an endless recursion loop in the framework. Nonetheless, I ended up with this code:
namespace App\Providers;
use App\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class UrlGeneratorServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton("url", function($app) {
$routes = $app['router']->getRoutes();
return new UrlGenerator( // this is actually my class due to the namespace above
$routes, $app->rebinding(
'request', $this->requestRebinder()
), $app['config']['app.asset_url']
);
});
}
protected function requestRebinder()
{
return function ($app, $request) {
$app['url']->setRequest($request);
};
}
}
And of course, registered the above provider in config/app.php under 'providers'
I'm working with multiple queues and want to push an event to another queue than the default one.
I have a standard event class that implements ShouldQueue, but how can I specify the queue this event will be pushed to?
I have tried with
class NewMessageArrived extends Event implements ShouldQueue
{
use SerializesModels;
protected $queue = 'redis';
....
but that doesn't do what I want. Is there a way to do this?
On Listener:
public $queue = 'notifications'; //Queue Name
public function queue(QueueManager $handler, $method, $arguments)
{
$handler->push($method, $arguments, $this->queue);
}
be happy =)
After spending some quality time with the Laravel source code, I'm fairly sure this isn't possible.
Events/Dispatcher.php's createClassCallable checks if your class has implements ShouldQueue, and if so, calls Dispatcher's createQueuedHandlerCallable() method, when in turn queues your event with a call to $this->resolveQueue()->push().
The problem is that push's third argument is what queue to push onto, and that argument is not passed. So it will always use the default queue, so you have no way of specifying an alternate default one.
I recommend queueing the event yourself with something like:
Queue::push(
function() {
event(new NewMessageArrived());
},
'',
'my-queue'
);
public function __construct()
{
$this->queue = 'high';
}
I tried to override $queue because it public but, laravel 5.6 return errors.
So, set the queue in the class construct is the shortest working solution i found.