Laravel Events is one of those features that is critical to many applications and the applications I am currently working on are no different. When I first started using the Events I found that the setup was very simple to use and everything made sense – fire an event when something happens and setup a listener response that will react. As the docs state, this really is a simple observer implementation. The example in the docs is quite simple and also a very useful example:
Event::listen('user.login', function($user) { $user->last_login = new DateTime; $user->save(); }); |
$event = Event::fire('user.login', array($user)); |
This implementation is quite simple, but there are a few things that can be missed if you haven’t worked with a similar system before.
- The event listener has to be declared before the event fires. This seems simple, but it caused me a few headaches when working in packages and realizing that everything had to be organized in a specific order so that the events would fire properly.
- Data can be passed to the listener closure, but this is not necessary. You could just write the same function without passing $user and use Auth::user() to the same ends.
- You can have multiple listeners for a single event and you can also give them a priority to decide in which order they will be triggered. Simply pass a third parameter after the closure with a numeric value (ie: 5). Keep in mind this is priority not order so the higher the number, the earlier it is triggered.
Putting it to Use
Everything above is more or less covered in the documentation and with a basic knowledge of how Events work. The next step I took was to start putting these into practice with a few simple examples that I found very useful.
Event::listen('user.login.failed', function($user) { Log::warning('Login Failed for User '.$user->username); $user->failed_attempts = $user->failed_attempts + 1; $user->save(); }); |
Event::listen('user.logout', function($user) { // Queue up session clean for this user // Remove any unpublished items in database for user }); |
Going Further With Events
After using Events extensively in smaller projects, we began a very large scale project at work that was going to be entirely package-based with a very small framework developed on top of Laravel that allowed us to create a global dashboard layout that had ways for packages to extend it and implement themselves inside it. For our purposes, this had to be more than just a layout – we needed to have one application that could be changed in it’s entirety for a second client without having to change the core code or our existing packages. Laravel and Composer make this very easy on the surface, but the hooks we needed inside the core were the biggest hurdle as to how we were going to implement this structure. Whether or not the following examples are the best way to do this is up for debate, but for our purposes it got the job done and it gave us an easy way to implement our extensibility.
Using Events as Hooks
The system we were building required that the main dashboard could have “hooks” that packages could grab onto and return data. We discussed a number of ways of doing this, but we decided that creating a rigid system for each hook made the most sense. What we ended up with was a well-documented system that allowed our packages to declare dashboard widgets, reporting widgets, CMS block types and necessary config values. Below is an example of one of these implementations.
Dashboard Widget
First thing we do is in our extending package’s service provider register method. We listen for the event widgets.dashboard.create and we return a very specific array of data that allows this package to define a widget. Note that we are returning an array of data from our listener – the Laravel docs don’t talk much about this.
\Event::listen('widgets.dashboard.create', function(){ return array( 'size' => 'col-md-4', 'icon' => 'fa-rub', 'heading' => 'Weekly Sales', 'text' => count($sales).' Items', 'link' => 'dashboard/orders/week', 'link_text' => 'View Sales', 'color' => 'info', 'type' => 'panel' ); }, 20); |
Now that we have the listener setup, the event fire is pretty self-explanatory. Note that we set the variable $widgets to the response of the fire method – when we do this Laravel returns an array of all the results. Because of this, we can listen 6 times to this event and the $widgets will contain an array of 6 widget arrays for us to work with, in the order defined by our event priority.
The rest of the code is not as relevant to the events package, but we takes those arrays and process them based on the type defined in the array and then pass that data to the view.
$widgets = \Event::fire('widgets.dashboard.create'); $data['widgets'] = array(); foreach($widgets as $w) { if(method_exists('\Company\Widgets\Widget', 'create_'.$w['type'])) { $data['widgets'][] = call_user_func('\Company\Widgets\Widget::create_'.$w['type'], $w); } } |
Config Variables
Another issue we needed to solve for was allowing one global configuration section to handle configuration for all packages. We used a similar setup to the widgets and then used a simple key-value pair in the database that would handle configuration dynamically based on what values the packages requested be added. We debated countless ways of doing this with passing input types and other data sets, but in the end we decided to allow each package to make their own config views and then pass the view along. We also wanted to ensure that each package could define a sub-config section without having to tell the core that it exists. The routes we used in combination with the controller allowed us to check if any config/{type} existed and, if it exists, display the relevant views.
// Pass the Orders Config View to the Config Builder \Event::listen('config.build.orders', function() { return 'Orders::config.orders'; }); // Pass the Main Config View to the Config Builder \Event::listen('config.build', function() { return 'Orders::config.main'; }); |
// Are we looking for a specific config page? // This code checks if we are on /config/$type or just on /config route. if(is_null($type)) { $views = \Event::fire('config.build'); if(count($views) > 0) { foreach($views as $v) { // Load each view file into a config array for our main config view $data['config'][] = \View::make($v); } } } // We have a specific type. Check if we have views registered. else { // Fire a dynamic type event to retrieve views for a specific type of config page $views = \Event::fire('config.build.'.$type); // Check if we have more than one view for this config type if(count($views) > 0) { foreach($views as $v) { $data['config'][] = \View::make($v); } } else { return \Redirect::to('config')->with('warning', 'Could Not Find That Config Page'); } } |
Final Thoughts
The one downfall to this system may end up being our overhead. The basic implementation isn’t very taxing, but as we continue to develop we will be looking for ways to avoid firing events if there won’t be a listener for that request. This could be as simple as only registering config listeners when “config” exists in the current route.
The other issue, as noted above, is that there is likely a better way to do some of this. We chose to do it this way because the Events package was already there and had things like priority and data passing capabilities that we would need and there were few shortcomings from a developer perspective to implementing the system this way. As we continue to work on the application we may move more towards Events or away from it if we find an alternative, but for now it’s meeting our needs.