Are cake events handled asynchronously? - events

At the moment I don't have any queuing functionality in my Cakephp aplication. I will need that in the near future. An upload will result in a batchjob that uses external API with usage limitations, so it would be best if it was handeled in a seperate threat with a queue.
I don't have any experience with this, so I'm going to try a different, but easier, example.
User actions result in e-mails being send. At the moment, the loading of the page is delayed by the (rather long) time it takes the server to send the e-mail. I'd like to use the Event system to fix this. (I am aware I can also do this using this the afterRender function, or dispatch it to a shellTask, but that way I don't learn anything)
From the example page:http://book.cakephp.org/2.0/en/core-libraries/events.html
I've found this example:
// Cart/Model/Order.php
App::uses('CakeEvent', 'Event');
class Order extends AppModel {
public function place($order) {
if ($this->save($order)) {
$this->Cart->remove($order);
$this->getEventManager()->dispatch(new CakeEvent('Model.Order.afterPlace', $this, array(
'order' => $order
)));
return true;
}
return false;
}
}
Let's say the function was called by a controller action:
public function place_order() {
$result = $this->Order->place($this->request->data);
$this->set('result', $result);
}
Now my question... Will the corresponding view be rendered after all the dispatched events completes? or will the Model function just trigger the event and then forget about it?
The last option seems more logical to me (which also resembles the mentioned jQuery functionality in the article)
The problem is that If this were true, I don't understand the later example:
In the example about using results:
// Using the event result
public function place($order) {
$event = new CakeEvent('Model.Order.beforePlace', $this, array('order' => $order));
$this->getEventManager()->dispatch($event);
if (!empty($event->result['order'])) {
$order = $event->result['order'];
}
if ($this->Order->save($order)) {
// ...
}
// ...
}
if the event was just triggered (and then forgot about) there is no way you can asume it has modified the passed event object on the next line of code!
I would like to use cake as much as possible, but I'm not sure if I can get my desired background behavior without shellTasks and external queue. Any tips about these Cake Events?

Cake Events are triggered synchronously. When an event is triggered, all available listeners are called, before proceeding with other instructions.
You can imagine it on your second example as:
public function place($order) {
$event = new CakeEvent('Model.Order.beforePlace', $this, array('order' => $order));
$this->getEventManager()->dispatch($event); // -> all listeners are called at this point
// ... here you can assume your $event was modified
if (!empty($event->result['order'])) {
$order = $event->result['order'];
}
if ($this->Order->save($order)) {
// ...
}
// ...
}

Related

How to stop event faking (toggle event faking) in Laravel unit testing

I am developing a Laravel application and doing the unit test. Now, I am having a bit of an issue with faking and un-faking Laravel event in the unit test. What I am trying to do is something like this.
public function test_something()
{
Event::fake();
//Do somethng
//Then I want to stop faking event here something like this
Event::stopFaking(); //maybe
}
I think my code is self-explanatory. How can I achieve something like that in Laravel?
https://laravel.com/docs/5.7/mocking
If you only want to fake event listeners for a portion of your test, you may use the fakeFor method:
$order = Event::fakeFor(function () {
$order = factory(Order::class)->create();
Event::assertDispatched(OrderCreated::class);
return $order;
});
// Events are dispatched as normal and observers will run ...
$order->update([...]);
Everything inside the function() {} will have faked events. Everything outside will function normally.
The Event::fake function is defined in Illuminate\Support\Facades\Event.
We can see that there is a fakeFor method that only fakes it during the execution of a callback method, then restores the original behavior. You can use it like this:
public function test_something()
{
Event::fakeFor(function () {
//Do somethng
});
}
As a Laravel developer it is often useful to read the source code, there are lots of nice bits and pieces in this framework that are not documented!

Laravel Cancel Creation of Object

I have a Laravel app which has an object, Position, which is created via a form.
class Position extends Model
{
protected $dispatchesEvents = [
'creating' => PositionCreating::class,
];
And this calls an event of the PositionCreating class, which I've tested, and is correctly firing. The underlying code also works to give me success or fail criteria.
class PositionCreating
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public function __construct(Position $position)
{
if (some_good_stuff())
{
//keep creating the object
} else {
//stop creating the object
}
}
If it works, that's fine, I just let the __construct() function finish executing and everything, including the pre-execution code I want, works perfectly.
But I don't know how to actually stop the creation of the object. I can, of course use the dd() function or something (which works and stops creation of the object as expected), but I want to present a readable error to the user in a friendly manner. What function or commands should I be using to cancel the creation of the object to return back to my position.create method?
A bit late answer but this is a way to do it. Models fire several events. The one you're looking for is probably the "created" event. Each model event receive an instance of the model so you could just attach an event on your model, just like this:
protected $dispatchesEvents = [
'created' => PositionCreated::class,
];
Inside your "PositionCreated" event, add a public property to get the model instance,like this:
public $position;
public function __construct(Position $position)
{
$this->position=$position;
}
Finally just add the logic on your "handle" method inside your event listener.
public function handle($event)
{
if($something)
{
$event->position->delete();
}
}
This should do the work.You can check for the other events and see wich one suits you the most.

Trouble with multiple model observers in Laravel

I'm stuck on a weird issue. It feels like in Laravel, you're not allowed to have multiple model observers listening to the same event. In my case:
Parent Model
class MyParent extends Eloquent {
private static function boot()
{
parent::boot();
$called_class = get_called_class();
$called_class::creating(function($model) {
doSomethingInParent();
return true;
}
}
}
Child Model
class MyChild extends myParent {
private static function boot()
{
parent::boot();
MyChild::creating(function($model) {
doSomethingInChild();
return true;
}
}
}
In the above example, if I do:
$instance = MyChild::create();
... the line doSomethingInChild() will not fire. doSomethingInParent(), does.
If I move parent::boot() within the child after MyChild::creating(), however, it does work. (I didn't confirm whether doSomethingInParent() fires, but I'm presuming it doesn't)
Can Laravel have multiple events registered to Model::creating()?
This one is tricky. Short version: Remove your return values from you handlers and both events will fire. Long version follows.
First, I'm going to assume you meant to type MyParent (not myParent), that you meant your boot methods to be protected, and not private, and that you included a final ) in your create method calls. Otherwise your code doesn't run. :)
However, the problem you describe is real. The reason for it is certain Eloquent events are considered "halting" events. That is, for some events, if any non-null value is returned from the event handlers (be it a closure or PHP callback), the event will stop propagating. You can see this in the dispatcher
#File: vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php
public function fire($event, $payload = array(), $halt = false)
{
}
See that third parameter $halt? Later on, while the dispatcher is calling event listeners
#File: vendor/laravel/framework/src/Illuminate/Events/Dispatcher.php
foreach ($this->getListeners($event) as $listener)
{
$response = call_user_func_array($listener, $payload);
// If a response is returned from the listener and event halting is enabled
// we will just return this response, and not call the rest of the event
// listeners. Otherwise we will add the response on the response list.
if ( ! is_null($response) && $halt)
{
array_pop($this->firing);
return $response;
}
//...
If halt is true and the callback returned anything that's not null (true, false, a sclaer value, an array, an object), the fire method short circuits with a return $response, and the events stop propagating. This is above and beyond that standard "return false to stop event propagation". Some events have halting built in.
So, which Model events halt? If you look at the definition of fireModelEvent in the base eloquent model class (Laravel aliases this as Eloquent)
#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
protected function fireModelEvent($event, $halt = true)
{
//...
}
You can see a model's events default to halting. So, if we look through the model for firing events, we see the events that do halt are
#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
$this->fireModelEvent('deleting')
$this->fireModelEvent('saving')
$this->fireModelEvent('updating')
$this->fireModelEvent('creating')
and events that don't halt are
#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
$this->fireModelEvent('booting', false);
$this->fireModelEvent('booted', false);
$this->fireModelEvent('deleted', false);
$this->fireModelEvent('saved', false);
$this->fireModelEvent('updated', false);
$this->fireModelEvent('created', false);
As you can see, creating is a halting event, which is why returning any value, even true, halted the event and your second listener didn't fire. Halting events are typically used when the Model class wants to do something with the return value from an event. Specifically for creating
#File: vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php
protected function performInsert(Builder $query)
{
if ($this->fireModelEvent('creating') === false) return false;
//...
if you return false, (not null) from your callback, Laravel will actually skip performing the INSERT. Again, this is different behavior from the standard stop event propagation by returning false. In the case of these four model events, returning false will also cancel the action they're listening for.
Remove the return values (or return null) and you'll be good to go.

Laravel 4 how to listen to a model event?

I want to have an event listener binding with a model event updating.
For instance, after a post is updated, there's an alert notifying the updated post title, how to write an event listener to have the notifying (with the post title value passing to the listener?
This post:
http://driesvints.com/blog/using-laravel-4-model-events/
Shows you how to set up event listeners using the "boot()" static function inside the model:
class Post extends eloquent {
public static function boot()
{
parent::boot();
static::creating(function($post)
{
$post->created_by = Auth::user()->id;
$post->updated_by = Auth::user()->id;
});
static::updating(function($post)
{
$post->updated_by = Auth::user()->id;
});
}
}
The list of events that #phill-sparks shared in his answer can be applied to individual modules.
The documentation briefly mentions Model Events. They've all got a helper function on the model so you don't need to know how they're constructed.
Eloquent models fire several events, allowing you to hook into various points in the model's lifecycle using the following methods: creating, created, updating, updated, saving, saved, deleting, deleted. If false is returned from the creating, updating, saving or deleting events, the action will be cancelled.
Project::creating(function($project) { }); // *
Project::created(function($project) { });
Project::updating(function($project) { }); // *
Project::updated(function($project) { });
Project::saving(function($project) { }); // *
Project::saved(function($project) { });
Project::deleting(function($project) { }); // *
Project::deleted(function($project) { });
If you return false from the functions marked * then they will cancel the operation.
For more detail, you can look through Illuminate/Database/Eloquent/Model and you will find all the events in there, look for uses of static::registerModelEvent and $this->fireModelEvent.
Events on Eloquent models are structured as eloquent.{$event}: {$class} and pass the model instance as a parameter.
I got stuck on this because I assumed subscribing to default model events like Event:listen('user.created',function($user) would have worked (as I said in a comment). So far I've seen these options work in the example of the default model user created event:
//This will work in general, but not in the start.php file
User::created(function($user)....
//this will work in the start.php file
Event::listen('eloquent.created: User', function($user)....
Event::listen('eloquent.created: ModelName', function(ModelName $model) {
//...
})

How to avoid errors in saving data - Cakephp

I'm using Cakephp and trying to put in a method to make sure our reservation system doesn't let two users book the same appointment. Ex. User 1 opens the appointment, and User 2 opens it simultaneously. User 1 books the appointment. User 2 tries to book it but the system checks and sees it is no longer available.
I imagine this would take place in validation, or in a beforeSave(), but can't figure out how to do it.
Right now I made a function in the model to call from the controller. In the controller I have:
if ($this->Timeslot->checkIfNotAvailable()) {
$this->Session->setFlash('This timeslot is no longer available');
$this->redirect(array('controller' => 'users', 'action' => 'partner_homepage'));
}
and in the model I have this function:
function checkIfNotAvailable($data) {
$this->recursive = -1;
$timeslot = $this->find('all', array(
'conditions' => array(
'Timeslot.id' => $this->data['Timeslot']['id'])
)
);
if ($timeslot['student_id'] == 0) {
//They can reserve it, do not spring a flag
return false;
} else {
//Throw a flag!
return true;
}
}
I think I'm mixed up using custom validation when it's not called for. And it's not working obviously. Any suggestions?
Thanks!
If what you have is working, you can stick with it, you could also try creating a beforeValidate() call back function in your Model.
class YourModel extends AppModel {
function beforeValidate(){
if( !$this->checkIfNotAvailable( $this->data ) ) {
unset($this->data['YourModel']['time_slot']);
}
return true; //this is required, otherwise validation will always fail
}
}
This way you remove the time_slot before it goes to validation and it will drop a validation error at that point, kicking the user back to the edit page and getting them to pick a different time slot, ideally the updated data entry page will no longer have the used time slot available.

Resources