Laravel Mailable queue, how to pass custom data to MessageSent event - laravel-mail

I am using Laravel Mailable to send email, and I want to log the email that was successfully sent.
Laravel Mailable have default event that was fired after the email was sent
https://laravel.com/docs/5.6/mail#events
So I hook my listener to this event
protected $listen = [
'App\Events\Event' => [
'App\Listeners\EventListener',
],
'Illuminate\Mail\Events\MessageSent' => [
'App\Listeners\LogSentEmailNotification',
],
];
Listener handler
public function handle(MessageSent $event)
{
//get extra data
$job_request_id = $event->message->job_request_id;
$message = $event->message;
$data = [
'job_request_id' => $job_request_id,
'to' => $message->getHeaders()->get('To'),
'from' => $message->getHeaders()->get('From'),
'cc' => $message->getHeaders()->get('Cc'),
'bcc' => $message->getHeaders()->get('Bcc'),
'subject' => $message->getHeaders()->get('Subject')->getFieldBody(),
'body' => $message->getBody(),
];
$email_notification_log = $this->email_notification_log->create($data);
}
The extra data job_request_id is passed from the build() method in Mailable class, CustomEmailNotification.php
class CustomEmailNotification extends Mailable implements ShouldQueue
{
public function build()
{
$job_request_id = 1;
//pass extra data mail message
$this->withSwiftMessage(function ($message) use($job_request_id){
$message->job_request_id = $job_request_id;
});
}
}
Right now this line on Listener class is working fine without queue, however when using queue it will return null
//get extra data
$job_request_id = $event->message->job_request_id;
var_dump($job_request_id);
//null when using queue
Question is, what is the correct way to pass custom data to MailSent event when using queue?
Or is there possibility job_request_id is lost when using queue and pass to withSwiftMessage(), so the Event Listener just received null value?
Thanks

Related

Trying to hook into Model 'updating' event with a trait

I'm trying to provide a way to track when a user makes a change to a model for a notes section in my application. E.g. John goes and modifies 2 fields, a note would be created saying John has changed title from 'My title 1' to 'My title 2' and content from 'Lipsum' to 'Lipsum2'.
Here is a trait I created:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
trait TrackChanges
{
public $changes;
public static function bootChangesTrait()
{
static::updating(function($model)
{
$this->changes = [];
foreach($model->getDirty() as $key => $value)
{
$original = $model->getOriginal($key);
$this->changes[$key] = [
'old' => $original,
'new' => $value,
];
}
});
}
}
And I am using that trait successfully on my model. However, I'm not sure how to capture the contents of the changes, or if they are even working correctly.
In my controller I have:
$site = Site::findOrFail($id);
// Catch and cast the is_active checkbox if it's been unselected
if ( ! $request->exists('is_active') )
{
$request->request->add([ 'is_active' => 0 ]);
}
// // Get rid of the CSRF field & method
$data = $request->except([ '_token', '_method' ]);
$site->update($data);
I tried dd($site->changes) before and after $site->update($data); but it just returns null.
What am I doing wrong?
You need to change your boot method in your trait to bootTrackChanges(). To boot traits you need to follow the naming pattern of boot{TraitName} for your boot method. Then you need to change your $this calls in your trait to $model so the change get saved to the model so your trait should look like this:
<?php
namespace App\Traits;
use Illuminate\Database\Eloquent\Model;
trait TrackChanges
{
public $changes;
public static function bootTrackChanges()
{
static::updating(function($model)
{
$changes = [];
foreach($model->getDirty() as $key => $value)
{
$original = $model->getOriginal($key);
$changes[$key] = [
'old' => $original,
'new' => $value,
];
}
$model->changes = $changes;
});
}
}
Another thing to note is if you have defined a boot method in your model make sure you call the parent boot method as well or else your trait's boot methods will not be called and your listener will not be registered.. I have spent hours and hours on this one before due to forgetting to call the parent method. In your model defining a boot method is not required but if you did call the parent like:
class MyModel extends Model
{
use TrackChanges;
protected static function boot()
{
// Your boot logic here
parent::boot();
}
}

ZF2 + Doctrine 2 - Use a Factory to create a Doctrine SQLFilter - How?

I figured it out, answer is below - Leaving the question and all the process stuff here in case it might help someone figure out the same in the future, though most of it is made redundant by the answer.
What I'm trying to do is, filter data based on the Company that a User is associated with (User#company).
I have a few Entities for this scenario:
User
Company
Address
The scenario is that data, in this case Address Entity objects, are created by User Entities. Each User has a Company Entity that it belongs to. As such, each Address has a Address#createdByCompany property.
Now I'm trying to create a SQLFilter extension, as described by the Doctrine docs - "Working with Filters".
I've created the following class:
class CreatedByCompanyFilter extends SQLFilter
{
/**
* #var Company
*/
protected $company;
/**
* #param ClassMetadata $targetEntity
* #param string $targetTableAlias
* #return string
* #throws TraitNotImplementException
*/
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
// Check if Entity implements CreatedByCompanyAwareInterface, if not, return empty string
if (!$targetEntity->getReflectionClass()->implementsInterface(CreatedByCompanyInterface::class)) {
return '';
}
if (!array_key_exists(CreatedByCompanyTrait::class, $targetEntity->getReflectionClass()->getTraits())) {
throw new TraitNotImplementException(
($targetEntity->getReflectionClass()->getName()) . ' requires "' . CreatedByCompanyTrait::class . '" to be implemented.'
);
}
return $targetTableAlias . '.created_by_company = ' . $this->getCompany()->getId();
}
/**
* #return Company
*/
public function getCompany(): Company // Using PHP 7.1 -> explicit return types
{
return $this->company;
}
/**
* #param Company $company
* #return CreatedByCompanyFilter
*/
public function setCompany(Company $company): CreatedByCompanyFilter
{
$this->company = $company;
return $this;
}
}
To use this filter, it's registered in the config and setup to be loaded in the modules Bootstrap (onBootstrap). So far so good, the above gets used.
However, the above is not used via the Factory Pattern. Also, you might notice that the addFilterConstraint(...) uses $this->getCompany()->getId(), but $this->setCompany() isn't called anywhere, creating a function call on a null value.
How can I use a Factory to create this class, either using the normal route of the ZF2 ServiceManager or via a registration of Doctrine itself?
What I've already tried
Next to Google'ing a lot for the past few hours, trying to find a solution, I've also tried the following
1 - ZF2 Factory
Using the default ZF2 method of registering the Factory in config, does not work:
'service_manager' => [
'factories' => [
CreatedByCompanyFilter::class => CreatedByCompanyFilterFactory::class,
],
],
The Factory simply never gets called. This could have something to do with the order of execution. I'm thinking Doctrine SQLFilters get set before the ServiceManager is fully up and running, just in case of a scenario I'm trying to do: filtering data for a user based on some role-based stuff (or "company-stuff" in this case).
2 - Ocramius's ZF2 Delegator Factories
While working on this I found Ocramius's Delegator Factories. Very interesting stuff, definitely worth a read and works nicely. However, not for my scenario. I followed his guide and created a CreatedByCompanyFilterDelegatorFactory. This I registered in the config, but had no result, the actual Factory never gets called.
(sorry, removed code already)
The Factory I'm trying to run Updated as *ListenerFactory, see 'Currently trying' below
class CreatedByCompanyListenerFactory implements FactoryInterface
{
// NOTE: REMOVED DOCBLOCKS/COMMENTS FOR LESS CODE
public function createService(ServiceLocatorInterface $serviceLocator)
{
$authService = $serviceLocator->get('zfcuser_auth_service');
$user = $authService->getIdentity();
$listener = new CreatedByCompanyListener();
$listener->setCompany($user->getCompany());
return $listener;
}
}
Currently trying
I figured I could try to take a page out of the Gedmo Extensions and Doctrine Events playbook and use the format of hooking a Listener onto the loadClassMetadata Event.
As an example, Gedmo's SoftDeleteable has the following config within Doctrine's config to make it work:
'eventmanager' => [
'orm_default' => [
'subscribers' => [
SoftDeleteableListener::class,
],
],
],
'configuration' => [
'orm_default' => [
'filters' => [
'soft-deleteable' => SoftDeleteableFilter::class,
],
],
],
So I figured, hell, let's try that, and setup the following:
'eventmanager' => [
'orm_default' => [
'subscribers' => [
CreatedByCompanyListener::class,
],
],
],
'configuration' => [
'orm_default' => [
'filters' => [
'created-by-company' => CreatedByCompanyFilter::class,
],
],
],
'service_manager' => [
'factories' => [
CreatedByCompanyListener::class => CreatedByCompanyListenerFactory::class,
],
],
The purpose would be to use the *ListenerFactory to get the authenticated User Entity into the *Listener. The *Listener in turn would pass on the Company associated with the User into the EventArgs passed along to the CreatedByCompanyFilter. That would, theoretically, having that object should be available.
The CreatedByCompanyLister is, for now, the following:
class CreatedByCompanyListener implements EventSubscriber
{
//NOTE: REMOVED DOCBLOCKS/HINTS FOR LESS CODE
protected $company;
public function getSubscribedEvents()
{
return [
Events::loadClassMetadata,
];
}
public function loadClassMetadata(EventArgs $eventArgs)
{
$test = $eventArgs; // Debug line, not sure on what to do if this works yet ;)
}
// $company getter/setter
}
However, I'm getting stuck on the Zend\Authentication\AuthenticationService used in the *ListenerFactory, it throws an exception when trying to get the identity of the User with $user = $authService->getIdentity();.
Internally this function continues into ZfcUser\Authentication\Storage\Db class, line 70, ($identity = $this->getMapper()->findById($identity);), where it crashes, throwing on getMapper():
Zend\ServiceManager\ServiceManager::get was unable to fetch or create an instance for doctrine.entitymanager.orm_default
Stepping over (xdebug in PhpStorm) that call somehow lets it continue for a bit further (bit of a wtf...), but then it throws:
An exception was raised while creating "zfcuser_user_mapper"; no instance returned
With a "*Exception*previous" that says:
Circular dependency for LazyServiceLoader was found for instance Doctrine\ORM\EntityManager
Independently I know what all of these errors mean, but I've never seen all of 'em get thrown by the same function call (->getMapper()) before. Any ideas?
I was, of course, thinking wayyyyyy too complicated. I've now got it running using the following classes:
CreatedByCompanyFilter
CreatedByCompanyListener
CreatedByCompanyListenerFactory
The only config that I needed was the following:
'listeners' => [
CreatedByCompanyListener::class,
],
'service_manager' => [
'factories' => [
CreatedByCompanyListener::class => CreatedByCompanyListenerFactory::class,
],
],
Note: did not register the filter in the Doctrine config. So do not do below if you wish to do the same!
'doctrine' => [
'eventmanager' => [
'orm_default' => [
'subscribers' => [
CreatedByCompanyListener::class,
],
],
],
'configuration' => [
'orm_default' => [
'filters' => [
'created-by-company' => CreatedByCompanyFilter::class,
],
],
],
],
Repeat: the above is not needed in this scenario - Though seeing what I've been up to, I can see how I (and others) might assume that it might be.
So, just 3 classes, one of which a registered Listener with a registered ListenerFactory in the ServiceManager config.
The logic has ended up being that the Listener needs to enable the Filter in with the Doctrine EntityManager, after which it is possible to set required parameters in the return value (the filter). Not sure if "Daredevel" has a Stackoverflow account, but thanks for this article! in which I noticed the enabling of a filter, following by setting its params.
So, the Listener enables the Filter and sets its params.
The CreatedByCompanyListener is as follows:
class CreatedByCompanyListener implements ListenerAggregateInterface
{
// NOTE: Removed code comments & docblocks for less code. Added inline for clarity.
protected $company; // Type Company entity
protected $filter; // Type CreatedByCompanyFilter
protected $entityManager; // Type EntityManager|ObjectManager
protected $listeners = []; // Type array
public function attach(EventManagerInterface $events)
{
$this->listeners[] = $events->attach(MvcEvent::EVENT_BOOTSTRAP, [$this, 'onBootstrap'], -5000);
}
public function detach(EventManagerInterface $events)
{
foreach ($this->listeners as $index => $listener) {
if ($events->detach($listener)) {
unset($this->listeners[$index]);
}
}
}
public function onBootstrap(MvcEvent $event)
{
$this->getEntityManager()->getConfiguration()->addFilter('created-by-company', get_class($this->getFilter()));
$filter = $this->getEntityManager()->getFilters()->enable('created-by-company');
$filter->setParameter('company', $this->getCompany()->getId());
}
// Getters & Setters for $company, $filter, $entityManager
}
The CreatedByCompanyListenerFactory is as follows:
class CreatedByCompanyListenerFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$authService = $serviceLocator->get('zfcuser_auth_service');
$entityManager = $serviceLocator->get(EntityManager::class);
$listener = new CreatedByCompanyListener();
$user = $authService->getIdentity();
if ($user instanceof User) {
$company = $user->getCompany();
} else {
// Check that the database has been created (more than 0 tables)
if (count($entityManager->getConnection()->getSchemaManager()->listTables()) > 0) {
$companyRepo = $entityManager->getRepository(Company::class);
$company = $companyRepo->findOneBy(['name' => 'Guest']);
} else {
// Set temporary company for guest user
$company = $this->tmpGuestCompany(); // Creates "new" Company, sets name, excludes in $entityManager from persisting/flushing it
}
}
$listener->setCompany($company);
$listener->setFilter(new CreatedByCompanyFilter($entityManager));
$listener->setEntityManager($entityManager);
return $listener;
}
}
Lastly, the CreatedByCompanyFilter needs to actually do something, so it as below:
class CreatedByCompanyFilter extends SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
// Check if Entity implements CreatedByCompanyAwareInterface, if not, return empty string
if (!$targetEntity->getReflectionClass()->implementsInterface(CreatedByCompanyInterface::class)) {
return '';
}
if (!array_key_exists(CreatedByCompanyTrait::class, $targetEntity->getReflectionClass()->getTraits())) {
throw new TraitNotImplementedException(
$targetEntity->getReflectionClass()->getName() . ' requires "' . CreatedByCompanyTrait::class . '" to be implemented.'
);
}
$column = 'created_by_company';
return "{$targetTableAlias}.{$column} = {$this->getParameter('company')}";
}
}
Why this works
The most important bit out of the above code is the following, based on Daredevel's tutorial linked earlier:
public function onBootstrap(MvcEvent $event)
{
$this->getEntityManager()->getConfiguration()->addFilter('created-by-company', get_class($this->getFilter()));
$filter = $this->getEntityManager()->getFilters()->enable('created-by-company');
$filter->setParameter('company', $this->getCompany()->getId());
}
This is where we add the Filter to the configuration of the EntityManager on the first line. This allows us to use it. Required is that you give the Filter a name and you tell the EntityManager which class belongs to the name with the second param.
Next, we enable() the Filter by name. Daredevel's tutorial showed me that the enable() function actually returns the filter just enabled.
Using the returned Filter in the $filter variable, we can now use that instance to set parameters, which we do in the last statement.
How is this different from what I tried in my question? Well, in my question I tried to do it the other way around. I tried both to enable the Filter via the config and via the Listener. However, with the latter method I tried creating and storing a Filter instance in a variable, setting the required parameters and then enabling it in the EntityManager, which is precisely the wrong way around as the enabling creates a new instance (and as such has no variables).
Therefore I'm leaving this here for anyone that might stumble upon the same issue.

Why my jwt tokens never expire?

I've inherited a Symfony project that uses this controller to authenticate users:
class TokenController extends FOSRestController
{
public function postTokensAction(Request $request)
{
$username = $request->request->get('username');
$password = $request->request->get('password');
$user = $this->get('fos_user.user_manager')
->findUserByUsername($username);
if (!$user) {
throw $this->createNotFoundException();
}
$passwordEncoder = $this->get('security.password_encoder');
if(!$passwordEncoder->isPasswordValid($user, $password)) {
throw $this->createAccessDeniedException();
}
$groups = ['foo', 'bar'];
$context = SerializationContext::create()
->setGroups($groups);
$token = $this->get('lexik_jwt_authentication.encoder')
->encode(['username' => $user->getUsername()]);
$user = $this->get('jms_serializer')
->toArray($user, $context);
return new JsonResponse([
'token' => $token,
'user' => $user
]);
}
}
And the customer requests an update: token should expire 10 seconds after the login. So, following the documentation, I added this listener.
<?php
namespace AppBundle\EventListener;
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
class JWTCreatedListener
{
public function onJWTCreated(JWTCreatedEvent $event)
{
$expiration = new \DateTime('now');
$expiration->add(new \DateInterval('PT10S'));
$payload = $event->getData();
$payload['exp'] = $expiration->getTimestamp();
$event->setData($payload);
}
}
And, of course, I've marked the listener to observe the event
acme_api.event.jwt_created_listener:
class: AppBundle\EventListener\JWTCreatedListener
tags:
- { name: kernel.event_listener, event: lexik_jwt_authentication.on_jwt_created, method: onJWTCreated }
If I get a token with Postman and use it to make following requests I can make those request for days and days. The token never expire. My JWTCreatedListener does not seem to work.
What's wrong?
They never expire because you are using a low level api which is the JWT encoder. As you can see (since you call it), encode() takes the payload.
For getting token expiration, the payload must contain the exp claim with the expiration timestamp as value.
This is handled by the lexik_jwt_authentication.jwt_manager service which uses the value of the lexik_jwt_authentication.encoder.token_ttl config option to determine the expiration date. Set it and uses $this->get('lexik_jwt_authentication.jwt_manager')->create($user) for creating the token, then $this->get('lexik_jwt_authentication.jwt_manager')->decode($token) at time to decode/verify it.
Note that for using this bundle properly (allowing to hook into all the events it provides), you should consider using proper security configuration (as shown in the README) instead of doing this by hand in your controller.
The key is here:
$token = $this->get('lexik_jwt_authentication.encoder')
->encode(['username' => $user->getUsername()]);
I need to add another parameter to encode function:
$token = $this->get('lexik_jwt_authentication.encoder')
->encode([
'username' => $user->getUsername(),
'exp' => (new \DateTime('+30 minute'))->getTimestamp(),
]);

Laravel Event Broadcast not work with pusher

I used pusher for my project. I configure broadcasting as per laravel docs. When I fired my event pusher does not work for me. But when I send data from pusher console then pusher receive this data.
I also try vinkla/pusher. Its work fine but laravel event broadcasting not work.
Please help me.
Here is my TestEvent.php code
namespace Factum\Events;
use Factum\Events\Event;
use Illuminate\Queue\SerializesModels;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
class TestEvent implements ShouldBroadcast
{
use SerializesModels;
public $text;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct($text)
{
$this->text = $text;
}
/**
* Get the channels the event should be broadcast on.
*
* #return array
*/
public function broadcastOn()
{
return ['test-channel'];
}
}
I encountered a similar problem and went step by step and fixed the issues.
I will assume you are running Laravel 5.3.
Here is a step by step walk through that might be helpful:
1) Check your config file: in config\broadcasting.php:
'connections' => [
pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_KEY'),
'secret' => env('PUSHER_SECRET'),
'app_id' => env('PUSHER_ID'),
'options' => [
'cluster' => 'eu',
'encrypted' => true,
// 'host' => 'api-eu.pusher.com'
// 'debug' => true,
],
],
2) Create a route for testing in your "web.php" file
Route::get('/broadcast', function() {
event(new \Factum\Events\TestEvent('Sent from my Laravel application'));
return ok;
});
3) In your "TestEvent.php" event file you should add this method in order to specify your event name:
/**
* The event's broadcast name.
*
* #return string
*/
public function broadcastAs()
{
return 'my_event';
}
4) Open your Pusher dashboard and go to debug console.
Keep the page open so you can notice if you got a successful request from your application.
5) Start or restart your queue worker. This step can make or brake everything. If you are using Mysql tables for queues I will assume that you already set up your "jobs" and "failed_jobs" database tables necessary for the queues.
Another important element is the worker - the queue processor.
Without an active worker running to process your queue, the jobs (TestEvent) will "remain" in the jobs table, meaning the jobs are pending and nothing will happen further until an active worker starts processing the queue.
You can start the worker like this:
www#yourmachine# php artisan queue:work --tries=3
6) Now that you have everything in order make a call on: "http://your-app.laravel/broadcast" and check your pusher debug console for a response.
Optional step:
If something is still missing, you can debug your app interaction with Pusher like so:
In your testing route try doing this:
Route::get('/broadcast', function() {
/* New Pusher instance with our config data */
$pusher = new \Pusher(config('broadcasting.connections.pusher.key'), config('broadcasting.connections.pusher.secret'), config('broadcasting.connections.pusher.app_id'), config('broadcasting.connections.pusher.options'));
/* Enable pusher logging - I used an anonymous class and the Monolog */
$pusher->set_logger(new class {
public function log($msg)
{
\Log::info($msg);
}
});
/* Your data that you would like to send to Pusher */
$data = ['text' => 'hello world from Laravel 5.3'];
/* Sending the data to channel: "test_channel" with "my_event" event */
$pusher->trigger( 'test_channel', 'my_event', $data);
return 'ok';
});
I hope it will work for you too!
Have a good time coding! ;)

Undefined eager-loaded model in Laravel Echo event

I'm using Laravel Echo to broadcast events from the server to the client.
The application is a forum, where users can create posts in topics.
Here is the controlled method code where the new post is created, and the event dispatched.
$post = Post::create([
'user_id' => 1,
'topic_id' => request('topic_id'),
'body' => request('body'),
]);
// Fetch the post we've just created, with the relationships this time
$post = Post::with('user', 'topic')->find($post->id);
// Broadcast the event
event(new PostCreated($post));
Here is the event class :
class PostCreated implements ShouldBroadcast
{
public $post;
public function __construct(Post $post)
{
$this->post = $post;
}
public function broadcastOn()
{
return new Channel('topics.' . $this->post->topic_id);
}
}
Finally, here is where the event is intercepted in the front-end :
Echo.channel('topics.' + this.topic.id)
.listen('PostCreated', (e) => {
this.posts.push(e.post);
});
The problem is that I can't seem to access the user property from the listen() method on the front-end.
console.log(e.post.user) // Undefined
If I do a console.log() of the post, I can see the properties of the Post (user_id, topic_id, body, created_at, updated_at) but it's not showing the user or topic properties that were eager-loaded in the controller, before the event was sent.
Those properties are accessible from the event class itself :
// In the __construct() method of the PostCreated event
echo $this->post->user->name; // Works, the name is echo'd
... but somehow are not sent to the front-end when the event is broadcasted.
How can I make sure that the user and topic properties are sent to the client, along with the post itself?

Resources