Test if Eloquent Observer`s method fires an event - php

The app I am working on fires an event, when one of the Eloquent model attributes is updated.
The Eloquent model is called Job and it is supposed to fire JobCustomerTotalAmountDueChanged when the duration attribute is updated. I have the following code in the JobObserver:
public function saved(Job $job)
{
if ($job->isDirty('duration')) {
event(new JobCustomerTotalAmountDueChanged($job));
}
}
When I try to test it using Event::fake, Eloquent events are not being fired, which means that the code in saved method is never execured. From what I see the assertDispatched and assertNotDispatched methods are only available for faked events. Is there a way to assert that a particular event is/is not fired without Event::fake?

The solution turned out to be very easy after all:
Laravel lets you specify which events should be faked, as an argument to fake method. In my example:
Event::fake([JobCustomerTotalAmountDueChanged::class])
All of the other events are being triggered and handled. Also, you can make assertions only with reference to events passed as argument to fake method. If you don't pass any argument, Laravel will try to 'fake' every event in the app.

Related

Laravel wait for a specific queued listener to finish then return

I have an Event with a bunch of queued listeners. I Can't run sync because I am calling external APIs etc
Events\Invoice\InvoiceEvent::class => [
Listeners\Invoice\Listener1::class, // should queue
Listeners\Invoice\Listener2::class, // should queue
Listeners\Invoice\Listener3::class, // Should NOT queue......
Listeners\Invoice\Listener4::class, // should queue
Listeners\Invoice\Listener5::class, // should queue
],
Calling this event from a controller method.
public function store(Request $request)
{
$invoice = Invoice::findOrFail($request->id);
InvoiceEvent::dispatch($invoice); // Async event, it cannot be sync
return $invoice; // need to return only when Listener3 finish execution
}
return $invoice is dependent on Listener3, otherwise, it will return incomplete data.
How can I return only when Listener3 is finished executing?
I came up with sleep(10); but it's not an ideal solution.
Listener3 saves data from third-party API to the invoices table which needs to be returned, that's why cannot return incomplete invoice data, right now the required data gets added to the invoice but after its return
PHP is natively synchronous. Unless you're pushing those events or listeners to a queue, (i.e. class Listener3 implements ShouldQueue) then they should run in order. However, you might want to rethink the structure of your code.
Listeners are best as reactions to an event, i.e. side effects, running independently from the rest of your application. Jobs, Events and Listeners should not generally return a value (except to halt a series of listeners). In your case, the Invoice is going through multiple steps, including calling a 3rd party API. Ideas:
Create a service class that performs the tasks on the Invoice and returns the invoice to the controller when completed (which then will return the $invoice data to the front end)
If you want the process to be async, consider using a push notification. Dispatch a job which performs the tasks on the Invoice, then alert the front end (e.g. Pusher ) when the invoice is ready to fetch.
There are a way to broadcast your event not queuing it,
into your event class add:
use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow;
so your class declaration must implements ShouldBroadcastNow
class NotQueueEvent implements ShouldBroadcastNow { ... }
this spread event without enqueue.
If you want to wait to this method return, you should only don't put it on the queue. Run the event and await the return. Don't know if I understood correctly the problem.

How to refresh the component after the queue job is finished?

I have a table products and job_statuses using this package: laravel-job-status
There is a column in job_statuses called status the package made this column finished once the queue job is finished!
So I created a column in products table called job_status_id (relation with job_statuses) just to save the job status id in the products table to see if this job is finished!
Simply I created a component using Livewire for just refresh single product when the job finished refresh the component:
class ProductIndex extends Component
{
public $product;
public function mount($product)
{
$this->product = $product;
}
public function render()
{
return view('livewire.merchant.product.index');
}
}
Inside product-index component:
#if ($product->job_status->status == 'finished')
// show real image
#else
// show loader
#endif
My blade:
#foreach ($products as $product)
<livewire:merchant.product.product-index :product="$product" :key="$product->id">
#endforeach
How can refresh the product component if the status is finished?
You can add an event-listener in your component, then fire an event from anywhere on the page - even from JavaScript - to refresh the component.
To add a listener to make the component refresh itself, simply add the following line to your ProductIndex component.
protected $listeners = ['refreshProducts' => '$refresh'];
Livewire will now listen for any refreshProducts events that are emitted, and once they are emitted, it will refresh that component.
You can also customize it further, by replacing the magic $refresh action with a method, which you can pass parameters to. If you name the event the same as the method, you don't need to specify a key/value pair (eventname as the key, methodname as the value), and the value alone will suffice. Here's an example,
protected $listeners = ['refreshProducts'];
// ...
public function refreshProducts($product_id = null)
{
// Refresh if the argument is NULL or is the product ID
if ($product_id === null || $product_id === $this->product->id) {
// Do something here that will refresh the event
}
}
From within a Livewire component, you can emit events using
$this->emit('refreshProducts');`
// or if you are passing arguments, as with the second example,
$this->emit('refreshProducts', $productID);
You can also emit events from JavaScript, by doing
<script>
Livewire.emit('refreshProducts')
</script>
If you want to make the queue trigger that event once its complete, you need to implement something that either polls the server to ask "has the job finished" and then fire the event, or you can use Laravel Echo as a websocket. This will allow you to fire and listen for events from outside the Livewire ecosystem.
Polling
When you're polling, you don't have to emit an event for every update, as the component will refresh itself continuously .
Polling is the easiest way to continuously update a Livewire component, it does not require any websockets integration like Laravel Echo. This means that every X seconds (default is 2 seconds), your component is going to make an AJAX request to your server, to fetch its newest data, and re-render itself with the new data.
This is easily achieved by wrapping the component with the wire:poll attribute - here's an example of using 5 seconds.
<div wire:poll.5s>
<!-- The rest of your view here -->
</div>
However, this means that all instances of that component will re-render themselves and fire an AJAX request of their own to the server to get the newest data. You might want to make a "parent" component for all your items, thereby just having 1 singular component re-render.
Broadcasting & Websockets
I'm going to assume that you have installed Laravel Echo already. Now - to achieve this functionality of broadcasting, you first need to create an event. Run the command
php artisan make:event ProductStatusUpdated
This will create an event in your app\Events folder. Customize it to however you need it. It will look something like this after running the command,
class ProductStatusUpdated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* #return void
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* #return \Illuminate\Broadcasting\Channel|array
*/
public function broadcastOn()
{
return new PrivateChannel('channel-name');
}
}
We can change the channel from PrivateChannel('channel-name') to Channel('products'), this allows us to listen for it in Livewire in the products channel (you could name the channel whatever you want, and you can also listen for private events - there's documentation for that in the Livewire documentation, referenced at the bottom of this answer).
So that means the broadcastOn would look something like this
public function broadcastOn()
{
return new Channel('products');
}
Next, after the job has completed its work and all the statuses has been set, fire that event from Laravel, using
event(new \App\Events\ProductStatusUpdated);
Now we need to update our listener in the Livewire component, so that we can actually listen for the broadcast on that channel through Laravel Echo (and not the Livewire events that we did before).
protected $listeners = ['echo:products,ProductStatusUpdated' => 'refreshProducts'];
And we're done! You're now using Laravel Echo to broadcast an event, which Livewire intercepts, and then runs. The listener is now a key/value pair, where the value is still the method name in the component (you can still use the magic action $refresh instead if you desire to), and the key is channel,event prefixed by echo:.
Resources:
https://laravel-livewire.com/docs/2.x/events
https://laravel-livewire.com/docs/2.x/laravel-echo

Laravel - Monitoring Record Change

I need an Eloquent model to represent a document uploaded to the server. One such field is the absolute path to the file. I want to be able to move the file whenever the database record is updated. I've seen that Laravel includes Observers to handle such events. The problem is:
Observers classes have method names which reflect the Eloquent events you wish to listen for. Each of these methods receives the model as their only argument.
This means that, unfortunately, with the given system I'm not able to actually move the file, since I will not have a way of retrieving both the current and new locations of the file in question. Is there another way of detecting when the value changes while having access to old and new values?
It sounds like you may be able to use the updating model event for this. You can register the event in the boot method of a service provider (such as app/Providers/AppServiceProvider.php) or by creating an observer class as you mentioned.
The isDirty and getOriginal methods should help you check if the file needs to be moved, and then get it's original path.
For example:
use Storage;
use App\Document;
public function boot()
{
Document::updating(function ($document) {
if ($document->isDirty('file_path')) {
// File needs to be moved
$current_path = $document->getOriginal('file_path');
$new_path = $document->file_path;
Storage::move($current_path, $new_path);
}
});
}
See also: Laravel updating eloquent event: getting the data

Symfony2 impersonate listener with parameters injection

i'm currently working on a custom logic impersonate listenener.
i overrided the Symfony\Component\Security\Http\Firewall\SwitchUserListener because i would like to perform call to some entities repositories ( not User entity ) before authorize the switch event :
for example , i would like to authorize the switch if and only if the user to impersonate has already gave rights to user requesting for the switch.
is it possible to inject new parameters such as doctrine service or some arrays values to an overrided listener ?
the call to my custom SwitchUserListenener :
in services.yml
parameters:
security.authentication.switchuser_listener.class: acme\appBundle\EventListener\SwitchUserListener
Your solution may be here:
https://github.com/symfony/symfony/blob/2.6/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php#L597
The service security.authentication.switchuser_listener defined by Symfony is an abstract service. It is not actually the service being used as the listener, but it is the same class.
Go up in the code a bit to:
https://github.com/symfony/symfony/blob/2.6/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php#L233
foreach ($firewalls as $name => $firewall) {
list($matcher, $listeners, $exceptionListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds);
This is the name of the firewall you created where the user switch will be possible.
Now you have a couple of options to override the listener.
You can continue to override the class parameter, then manipulate the arguments as Symfony is already doing:
$listener->replaceArgument(1, new Reference($userProvider));
$listener->replaceArgument(3, $id);
$listener->replaceArgument(6, $config['parameter']);
$listener->replaceArgument(7, $config['role']);
Or more simply, compile additional method calls in your extension.
The other solution, would be to create a CompilerPass which systematically deletes the original listeners and replaces them with your own. This might be achievable in your own bundles extension provided it is loaded after the SecurityBundle.
$container->removeDefinition($id);
EDIT:
Another solution, perhaps more simple than the above, is to create another event listener.
One which fires on the request. The important thing here is the listener must have a higher priority to the listeners you are overriding (I used listener(s) because there are potentially more than one switch user listener).
The tricky part will be getting a list of the overridden listeners IDs, because you are going to need to loop over them and call your own methods to inject your new dependencies after the listeners are instantiated, but before they triggered.

Laravel queues & Persistent Variables across classes

There i am diving into the world of queues and all of its goodness and it hit me:
Session data is lost when the application pushes a task to the queue, due to serialization of information by laravel.
Having found out how to send data to queues, a question remains:
Given that the queue pushes information to a single class,
how do i make that information persistent(such as a session) across other classes throughout the duration of this task?
Coding Example:
//Case where the user object is needed by each class
class queueme {
...
//function called by queue
function atask($job,$data)
{
//Does xyz
if(isset($data['user_id'])
{
//Push user_id to another class
anotherclass::anothertask($data['user_id']);
}
}
}
class anotherclass {
...
function anothertask($user_id)
{
//Does abc
//Yup, that anotherofanother class needs user_id, we send it again.
anotherofanotherclass::yetanothertask($user_id);
}
}
The above code illustrates my problem.
Do i have to pass the $user_id or User object around, if my classes need this information?
Isn't there a cleaner way to do it?
When you queue up a job, you should pass all data required by the job to do its work. So if it's a job to resize a user's avatar, the necessary information required is the primary key of the user so we can pull their model out in the job. Just like if you're viewing a user's profile page in the browser, the necessary information (the user's primary key) is likely provided in the request URI (e.g. users/profile/{id}).
Sessions won't work for queue jobs, because sessions are used to carry state over from browser requests, and queue jobs are run by the system, so they simply don't exist. But that's fine, because it's not good practice for every class to be responsible for looking up data. The class that handles the request (a controller for an HTTP request, or a job class for a queue job) can take the input and look up models and such, but every call thereafter can pass those objects around.
Back to the user avatar example. You would pass the ID of the user as a primitive when queueing the job. You could pass the whole user model, but if the job is delayed for a long time, the state of that user could have changed in the meanwhile, so you'd be working with inaccurate data. And also, as you mention, not all objects can be serialised, so it's best to just pass the primary key to the job and it can pull it fresh from the database.
So queue your job:
Queue::push('AvatarProcessor', [$user->id]);
When your job is fired, pull the user fresh from the database and then you're able to pass it around to other classes, just like in a web request or any other scenario.
class AvatarProcessor {
public function fire($job, $data)
{
$user_id = $data[0]; // the user id is the first item in the array
$user = User::find($user_id); // re-pull the model from the database
if ($user == null)
{
// handle the possibility the user has been deleted since
// the job was pushed
}
// Do any work you like here. For an image manipulation example,
// we'll probably do some work and upload a new version of the avatar
// to a cloud provider like Amazon S3, and change the URL to the avatar
// on the user object. The method accepts the user model, it doesn't need
// to reconstruct the model again
(new ImageManipulator)->resizeAvatar($user);
$user->save(); // save the changes the image manipulator made
$job->delete(); // delete the job since we've completed it
}
}
As mentioned by maknz, the data needs to be passed explicitly to the job. But in the job handle() method, you can use session():
public function handle()
{
session()->put('query_id', 'H123214e890890');
Then your variable is directly accessible in any class:
$query_id = session()->get('query_id')

Categories