Dispatch Error Event Not Connecting to Shared Event on AbstractController - events

My Error Page is not working as planned. I have an event that I create inside my Application Module with the onBootstrap Method that handles populating my design assets. It works on all pages except for the page where a route does not match I think its because when a dispatch error event occurs it executes all my events except for those attached by the shared event manager against Zend\Mvc\Controller\AbstractController
Here is the code for event attachment of my design event:
public function attach(EventManagerInterface $events,$priority=10) {
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH,[$this,'initiateAssets'],1000);
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH,[$this,'changeLayout'],30);
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH,[$this,'loadJsAssets'],30);
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH,[$this,'loadCssAssets'],30);
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH,[$this,'loadMetatagAssets'],30);
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH,[$this,'setupNavigation'],30);
}
Here is my event attachment inside the Application Module Bootstrap Function for when a route does not match.
$eventManager->attach(
\Zend\Mvc\MvcEvent::EVENT_DISPATCH_ERROR,
function ($e) {
$application = $e->getApplication();
$serviceLocator = $application->getServiceManager();
$match = $application->getMvcEvent()->getRouteMatch();
if (null === $match) {
$uri = $e->getRequest()->getUri();
$params = [
'__NAMESPACE__' => 'Application\Controller',
'controller' => 'Application\Controller\IndexController',
'action' => 'not-found',
'locale' => 'en_US'
// Here you can add common params for your application routes
];
$routeMatch = new \Zend\Router\Http\RouteMatch($params);
$routeMatch->setMatchedRouteName('notFound');
$application->getMvcEvent()->setRouteMatch(
$routeMatch
);
}
}
Im fairly certain the issue has to do with me attaching the design event to the shared event manager, which must be overridden during a dispatch event error against a separate controller, although i'm not quite sure
I did recall that my design assets are linked to the specific module,controller, and action. I have in my database the route for error as application module, index controller, and not-found action maybe this is incorrect and the design event is just not recognizing the mapped route (basically what is the correct routing information for this?)
It also seems to be producing the same result with a generic error as it shows the error/index file template but my design event did not populate the design assets.

Did you try to attach the EVENT_DISPATCH_ERROR:
public function attach(EventManagerInterface $events,$priority=10) {
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH_ERROR,[$this,'initiateAssets'],1000);
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH_ERROR,[$this,'changeLayout'],30);
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH_ERROR,[$this,'loadJsAssets'],30);
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH_ERROR,[$this,'loadCssAssets'],30);
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH_ERROR,[$this,'loadMetatagAssets'],30);
$events->getSharedManager()->attach('Zend\Mvc\Controller\AbstractController',
MvcEvent::EVENT_DISPATCH_ERROR,[$this,'setupNavigation'],30);
}

Related

Laravel Livewire Echo configuration

I've setup a notification system with Pusher and Echo on my Laravel 8 app. It works fine, I'm able to retrieve the notification event in VanillaJS with
window.Echo.private('App.Models.User.' + User.id)
.notification((notification) => {
if (notification.type === 'App\\Notifications\\JobLiked') {
let count = document.getElementById('count');
let number = count.innerHTML;
number++;
count.innerHTML = number;
}
});
But now I want to use Livewire listeners to trigger my function, then I setup :
public function getListeners()
{
return [
"echo-private:App.Models.User.{$this->authId},NotificationSent" => 'notifyNewJobLiked',
];
}
But nothing seems to work and I have no error message.. do you have any clue what could possibly going on ?
Thank you very much ! :)
Try to configure your listener with the following specific event name:
public function getListeners()
{
return [
"echo-private:App.Models.User.{$this->authId},.Illuminate\\Notifications\\Events\\BroadcastNotificationCreated" => 'notifyNewJobLiked',
];
}
Since you are using Laravel Notifications to trigger the broadcast instead of a broadcastable event, the event name when fired defaults to Illuminate\\Notifications\\Events\\BroadcastNotificationCreated.
In Echo there are two methods to listen for incoming messages: notification and listen. The reason why it works with your vanilla js is that you are using the notification method, whereas the livewire event listener only works with Echo's listen, which expects the name of the calling event.
If you are using pusher, you can see the name of the calling event in the pusher debug console.
Also take care and add a dot in front of the namespaced event as described in the documentation.
Right way to get Echo notification in livewire:
public function getListeners()
{
$user_id = auth()->user()->id;
return [
"echo-notification:App.Models.User.{$user_id}, notification" => 'gotNotification'
];
}
As you can see the channel type is notification, not private:
echo-notification
Also don't forget to have a record authenticating the users in your channels route, same as you do for a private channel:
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
return (int) $user->id === (int) $id;
});
Another thing I learned is that you can get the notification channel data from the returned field.
So you can do:
public function gotNotification($notification)
{
}

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) {
//...
})

Are cake events handled asynchronously?

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)) {
// ...
}
// ...
}

Resources