I'm struggling to get the final link in a CakePHP (v3.x) event working. In my Controler add method I have public function
add()
{
$event = new Event('Model.Comment.created', $this, [
'comment' => $comment
]);
$this->eventManager()->dispatch($event);
}
and have my listener class set up:
namespace App\Event;
use Cake\Log\Log;
use Cake\Event\EventListener;
class CommentListener implements EventListener {
public function implementedEvents() {
return array(
'Model.Comment.created' => 'updatePostLog',
);
}
public function updatePostLog($event, $entity, $options) {
Log::write(
'info',
'A new comment was published with id: ' . $event->data['id']);
}
}
But can't get the listener set up correctly, particularly with my app knowing that my CommentListener class exists.
I had the exact same issue, then I found this post:
Events in CakePHP 3 – A 4 step HowTo
It really cleared things up for me and describes that last linking step that you are needing. Assuming that your Listener class is in the Event folder under src of your app, all you need to do is step 4 in the article, I've adapted their code example to your example:
Lastly we have to register this listener. For this we will use the globally available EventManager. Place the following code at the end of your config/bootstrap.php
use App\Event\CommentListener;
use Cake\Event\EventManager;
$CommentListener = new CommentListener();
EventManager::instance()->attach($CommentListener);
The above is a global listener. It's also possible to register the event on the Model or Controller+Views layer as per the CakePhp docs (CakePHP 3.x Events System). It suggests between the lines that you can register the listener on the layer you require - so possibly the AppController on the beforeFilter callback or initialize method, although I've only tested the beforeFilter callback.
Update as of CakePHP 3.0.0 and forward
The function attach() has now been deprecated. The replacement function is called on() and therefore the code should look like this:
use App\Event\CommentListener;
use Cake\Event\EventManager;
$CommentListener = new CommentListener();
EventManager::instance()->on($CommentListener); // REPLACED 'attach' here with 'on'
Related
I have a "standard procedure" that I need in EVERY route I call in Symfony.
I basically create a short (relatively) unique number, check the permission and log that the function has been called.
Here is one example:
**
* #Route("/_ajax/_saveNewClient", name="saveNewClient")
*/
public function saveNewClientAction(Request $request)
{
/* Create Unique TrackNumber */
$unique= $this->get('log')->createUnique();
/* Check Permission */
if (!$permission = $this->get('permission')->needsLevel(2, $unique)) {
/* Log Action */
$this->get('log')->writeLog('No Permission, 2 needed', __LINE__, 4);
return new JsonResponse(array(
'result' => 'error',
'message' => 'Insufficient Permission'
)
);
}
/* Log Action */
$this->get('log')->writeLog('called '.__FUNCTION__, __LINE__, 1, $unique);
return $this->render(':admin:index.html.twig', array());
}
Is there a way to put all that in one function somewhere?
The writeLog gets called at other parts in the functions as well, so I don't want to combine it with the permisson check, although that would be possible of course.
When creating an EventListener, do I still have to call it in every function or is it possible to have it automatically called?
Any hint appreciated!
You could try to make a beforefilter.
http://symfony.com/doc/current/cookbook/event_dispatcher/before_after_filters.html
How to Set Up Before and After Filters
It is quite common in web application development to need some logic to be executed just before or just after your controller actions acting as filters or hooks.
Some web frameworks define methods like preExecute() and postExecute(), but there is no such thing in Symfony. The good news is that there is a much better way to interfere with the Request -> Response process using the EventDispatcher component.
Token Validation Example
Imagine that you need to develop an API where some controllers are public but some others are restricted to one or some clients. For these private features, you might provide a token to your clients to identify themselves.
So, before executing your controller action, you need to check if the action is restricted or not. If it is restricted, you need to validate the provided token.
Please note that for simplicity in this recipe, tokens will be defined in config and neither database setup nor authentication via the Security component will be used.
Before Filters with the kernel.controller Event
First, store some basic token configuration using config.yml and the parameters key:
YAML
# app/config/config.yml
parameters:
tokens:
client1: pass1
client2: pass2
Tag Controllers to Be Checked
A kernel.controller listener gets notified on every request, right before the controller is executed. So, first, you need some way to identify if the controller that matches the request needs token validation.
A clean and easy way is to create an empty interface and make the controllers implement it:
namespace AppBundle\Controller;
interface TokenAuthenticatedController
{
// ...
}
A controller that implements this interface simply looks like this:
namespace AppBundle\Controller;
use AppBundle\Controller\TokenAuthenticatedController;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
class FooController extends Controller implements TokenAuthenticatedController
{
// An action that needs authentication
public function barAction()
{
// ...
}
}
Creating an Event Listener
Next, you'll need to create an event listener, which will hold the logic that you want executed before your controllers. If you're not familiar with event listeners, you can learn more about them at How to Create Event Listeners and Subscribers:
// src/AppBundle/EventListener/TokenListener.php
namespace AppBundle\EventListener;
use AppBundle\Controller\TokenAuthenticatedController;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class TokenListener
{
private $tokens;
public function __construct($tokens)
{
$this->tokens = $tokens;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
/*
* $controller passed can be either a class or a Closure.
* This is not usual in Symfony but it may happen.
* If it is a class, it comes in array format
*/
if (!is_array($controller)) {
return;
}
if ($controller[0] instanceof TokenAuthenticatedController) {
$token = $event->getRequest()->query->get('token');
if (!in_array($token, $this->tokens)) {
throw new AccessDeniedHttpException('This action needs a valid token!');
}
}
}
}
Registering the Listener
Finally, register your listener as a service and tag it as an event listener. By listening on kernel.controller, you're telling Symfony that you want your listener to be called just before any controller is executed.
YAML
# app/config/services.yml
services:
app.tokens.action_listener:
class: AppBundle\EventListener\TokenListener
arguments: ['%tokens%']
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
With this configuration, your TokenListener onKernelController method will be executed on each request. If the controller that is about to be executed implements TokenAuthenticatedController, token authentication is applied. This lets you have a "before" filter on any controller that you want.
I need to use a custom Implementation of UrlGenerator. So how can I change the default binding of laravel, that is implemented somewhere deep in the core as
'url' => ['Illuminate\Routing\UrlGenerator', 'Illuminate\Contracts\Routing\UrlGenerator'],
against my own implementation?
Furthermore I am not shure. I assume this line above does actually two things. it will store the bindinung under the key "url" and it will also do the mapping of the Interface to the class. So I actually need to override both! How to do that? Furthemore how to find out if this must be bound as "shared"(singleton) or "new instance every time"?
Thanks very much!
Take a look at the Service Container guide http://laravel.com/docs/5.1/container
In this specific case I think all you need to do is to tell the app to replace the alias that already exists.
To do that I would recommend creating a ServiceProvider, registering int the config/app.php file and inside that one in the register method put something like:
$this->app->bind('Illuminate\Routing\UrlGenerator', 'yourownclasshere');
Let us know if it works.
Update: I removed the option that didn't work and left only the one that worked.
I did what Nestor said in his answer, but it didn't quite work for me. So this is what I did to make it work.
Inside my service provider in method register I first tried this:
$this->app->bind('url', MyCustomProvider::class);
This did register my URL provider instead of the default one. The problem was that now my provider didn't have any access to routes. I checked the Laravel code for \Illuminate\Routing\RoutingServiceProvider because it has a method registerUrlGenerator for registering the URL provider. This method did a direct instantiation of the Laravel URL generator Illuminate\Routing\UrlGenerator and giving proper parameters in the constructor.
So, I did the same in my service provider. Instead of doing $this->app->bind I did $this->app->singleton('url', function ($app) { ... }) and provided basically the same code in the closure function as in RoutingServiceProvider::registerUrlGenerator but created the instance of my URL generator. This then worked properly, and my generator is now called every time. The final code was this:
// the code is copied from the \Illuminate\Routing\RoutingServiceProvider::registerUrlGenerator() method
$this->app->singleton('url', function ($app) {
/** #var \Illuminate\Foundation\Application $app */
$routes = $app['router']->getRoutes();
$app->instance('routes', $routes);
// *** THIS IS THE MAIN DIFFERENCE ***
$url = new \My\Specific\UrlGenerator(
$routes,
$app->rebinding(
'request',
static function ($app, $request) {
$app['url']->setRequest($request);
}
),
$app['config']['app.asset_url']
);
$url->setSessionResolver(function () {
return $this->app['session'] ?? null;
});
$url->setKeyResolver(function () {
return $this->app->make('config')->get('app.key');
});
$app->rebinding('routes', static function ($app, $routes) {
$app['url']->setRoutes($routes);
});
return $url;
});
I hate copying the code, so it seems to me that the problem is in the base implementation. It should take the correct contract for URL generator instead of making direct instantiation of a base class.
I tried the Kosta's approach but it didn't fully work for me because it somehow created an endless recursion loop in the framework. Nonetheless, I ended up with this code:
namespace App\Providers;
use App\Routing\UrlGenerator;
use Illuminate\Support\ServiceProvider;
class UrlGeneratorServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton("url", function($app) {
$routes = $app['router']->getRoutes();
return new UrlGenerator( // this is actually my class due to the namespace above
$routes, $app->rebinding(
'request', $this->requestRebinder()
), $app['config']['app.asset_url']
);
});
}
protected function requestRebinder()
{
return function ($app, $request) {
$app['url']->setRequest($request);
};
}
}
And of course, registered the above provider in config/app.php under 'providers'
I've been through the documentation and all the articles on Yii2 events found using Google. Can someone provide me a good example of how events can be used in Yii2 and where it may seem logical?
I can explain events by a simple example. Let's say, you want to do few things when a user first registers to the site like:
Send an email to the admin.
Create a notification.
you name it.
You may try to call a few methods after a user object is successfully saved. Maybe like this:
if($model->save()){
$mailObj->sendNewUserMail($model);
$notification->setNotification($model);
}
So far it may seem fine but what if the number of requirements grows with time? Say 10 things must happen after a user registers himself? Events come handy in situations like this.
Basics of events
Events are composed of the following cycle.
You define an event. Say, new user registration.
You name it in your model. Maybe adding constant in User model. Like const EVENT_NEW_USER='new_user';. This is used to add handlers and to trigger an event.
You define a method that should do something when an event occurs. Sending an email to Admin for example. It must have an $event parameter. We call this method a handler.
You attach that handler to the model using its a method called on(). You can call this method many times you wish - simply you can attach more than one handler to a single event.
You trigger the event by using trigger().
Note that, all the methods mentioned above are part of Component class. Almost all classes in Yii2 have inherited form this class. Yes ActiveRecord too.
Let's code
To solve above mentioned problem we may have User.php model. I'll not write all the code here.
// in User.php i've declared constant that stores event name
const EVENT_NEW_USER = 'new-user';
// say, whenever new user registers, below method will send an email.
public function sendMail($event){
echo 'mail sent to admin';
// you code
}
// one more hanlder.
public function notification($event){
echo 'notification created';
}
One thing to remember here is that you're not bound to create methods in the class that creates an event. You can add any static, non static method from any class.
I need to attach above handlers to the event . The basic way I did is to use AR's init() method. So here is how:
// this should be inside User.php class.
public function init(){
$this->on(self::EVENT_NEW_USER, [$this, 'sendMail']);
$this->on(self::EVENT_NEW_USER, [$this, 'notification']);
// first parameter is the name of the event and second is the handler.
// For handlers I use methods sendMail and notification
// from $this class.
parent::init(); // DON'T Forget to call the parent method.
}
The final thing is to trigger an event. Now you don't need to explicitly call all the required methods as we did before. You can replace it by following:
if($model->save()){
$model->trigger(User::EVENT_NEW_USER);
}
All the handlers will be automatically called.
For "global" events.
Optionally you can create a specialized event class
namespace your\handler\Event\Namespace;
class EventUser extends Event {
const EVENT_NEW_USER = 'new-user';
}
define at least one handler class:
namespace your\handler\Event\Namespace;
class handlerClass{
// public AND static
public static function handleNewUser(EventUser $event)
{
// $event->user contain the "input" object
echo 'mail sent to admin for'. $event->user->username;
}
}
Inside the component part of the config under (in this case) the user component insert you event:
'components' => [
'user' => [
...
'on new-user' => ['your\handler\Event\Namespace\handlerClass', 'handleNewUser'],
],
...
]
Then in your code you can trigger the event:
Yii::$app->user->trigger(EventUser::EVENT_NEW_USER, new EventUser($user));
ADD
You can also use a closure:
allows IDE to "detect" the use of the function (for code navigation)
put some (small) code that manage the event
example:
'components' => [
'user' => [
...
'on new-user' => function($param){ your\handler\Event\Namespace\handlerClass::handleNewUser($param);},
'on increment' => function($param){ \Yii::$app->count += $param->value;},
],
...
]
By default Yii2 already provide some event declaration, You can read more about explanation on BaseActiveRecord.
You can use this variable as same as declaring it manually.
public function init()
{
parent::init();
$this->on(self::EVENT_AFTER_INSERT, [$this, 'exampleMethodHere']);
}
I'm trying to use the Event System in CakePHP v2.1+
It appears to be quite powerful, but the documentation is somewhat vague. Triggering the event seems pretty straight-forward, but I'm not sure how to register the corresponding listener(s) to listen for the event. The relevant section is here and it offers the following example code:
App::uses('CakeEventListener', 'Event');
class UserStatistic implements CakeEventListener {
public function implementedEvents() {
return array(
'Model.Order.afterPlace' => 'updateBuyStatistic',
);
}
public function updateBuyStatistic($event) {
// Code to update statistics
}
}
// Attach the UserStatistic object to the Order's event manager
$statistics = new UserStatistic();
$this->Order->getEventManager()->attach($statistics);
But it does not say where this code should reside. Inside a specific controller? Inside the app controller?
In case it's relevant, the listener will be part of a plugin which I am writing.
Update:
It sounds like a popular way to do this is by placing the listener registration code in the plugin's bootstrap.php file. However, I can't figure out how to call getEventManager() from there because the app's controller classes, etc aren't available.
Update 2:
I'm also told that listeners can live inside Models.
Update 3:
Finally some traction! The following code will successfully log an event when inside of my MyPlugin/Config/bootstrap.php
App::uses('CakeEventManager', 'Event');
App::uses('CakeEventListener', 'Event');
class LegacyWsatListener implements CakeEventListener {
public function implementedEvents() {
return array(
'Controller.Attempt.complete' => 'handleLegacyWsat',
);
}
public static function handleLegacyWsat($event) { //method must be static if used by global EventManager
// Code to update statistics
error_log('event from bootstrap');
}
}
CakeEventManager::instance()->attach(array('LegacyWsatListener', 'handleLegacyWsat'), 'Controller.Attempt.complete');
I'm not sure why, but I can't get errors when I try to combine the two App::uses() into a single line.
Events
Events are callbacks that are associated to a string. An object, like a Model will trigger an event using a string even if nothing is listening for that event.
CakePHP comes pre-built with internal events for things like Models. You can attach an event listener to a Model and respond to a Model.beforeSave event.
The EventManager
Every Model in Cake has it's own EventManager, plus there is a gobal singleton EventManager. These are not all the same instance of EventManager, and they work slightly differently.
When a Model fires an event it does so using the EventManager reference it has. This means, you can attach an event listener to a specific Model. The advantages are that your listener will only receive events from that Model.
Global listeners are ones attached to the singleton instance of EventManager. Which can be accessed anywhere in your code. When you attach a listener there it's called for every event that happens no matter who triggers it.
When you attach event listener in the bootstrap.php of an app or plugin, then you can use the global manager, else you have to get a reference to the Model you need using ClassRegistry.
What EventManager To Use?
If the event you want to handle is for a specific Model, then attach the listener to that Model's EventManager. To get a reference of the model you can call the ClassRegistry::init(...).
If the event you want to handle could be triggered anywhere, then attach the listener to the global EventManager.
Only you know how your listener should be used.
Inside A Listener
Generally, you put your business logic into models. You shouldn't need to access a Controller from an event listener. Model's are much easier to access and use in Cake.
Here is a template for creating a CakeEventListener. The listener is responsible for monitoring when something happens, and then passing that information along to another Model. You should place your business logic for processing the event in Models.
<?php
App::uses('CakeEventListener', 'Event');
class MyListener implements CakeEventListener
{
/**
*
* #var Document The model.
*/
protected $Document;
/**
* Constructor
*/
public function __construct()
{
// get a reference to a Model that we'll use
$this->Document = ClassRegistry::init('Agg.Document');
}
/**
* Register the handlers.
*
* #see CakeEventListener::implementedEvents()
*/
public function implementedEvents()
{
return array(
'Model.User.afterSave'=>'UserChanged'
);
}
/**
* Use the Event to dispatch the work to a Model.
*
* #param CakeEvent $event
* The event object and data.
*/
public function UserChanged(CakeEvent $event)
{
$data = $event->data;
$subject = $event->subject();
$this->Document->SomethingImportantHappened($data,$subject);
}
}
What I like to do is place all my Events into the Lib folder. This makes it very easy to access from anywhere in the source code. The above code would go into App/Lib/Event/MyListener.php.
Attaching The EventListeners
Again, it depends on what events you need to listen for. The first thing you have to understand is that an object must be created in order to fire the event.
For example;
It's not possible for the Document model to fire Model.beforeSave event when the Calendar controller is displaying an index, because the Calendar controller never uses the Document model. Do you need to add a listener to Document in the bootstrap.php to catch when it saves? No, if Document model is only used from the Documents controller, then you only need to attach the listener there.
On the other hand, the User model is used by the Auth component almost every. If you want to handle a User being deleted. You might have to attach an event listener in the bootstrap.php to ensure no deletes sneak by you.
In the above example we can attach directly to the User model like so.
App::uses('MyListener','Lib');
$user = ClassRegistry::init('App.User');
$user->getEventManager()->attach(new MyListener());
This line will import your listener class.
App::uses('MyListener','Lib');
This line will get an instance of the User Model.
$user = ClassRegistry::init('App.User');
This line creates a listener, and attaches it to the User model.
$user->getEventManager()->attach(new MyListener());
If the User Model is used in many different places. You might have to do this in the bootstrap.php, but if it's only used by one controller. You can place that code in the beforeFilter or at the top of the PHP file.
What About Global EventManager?
Assuming we need to listen for general events. Like when ever any thing is saved. We would want to attach to the global EventManager. It would go something like this, and be placed in the bootstrap.php.
App::uses('MyListener','Lib');
CakeEventManager::instance()->attach(new MyListener());
If you want to attach an event listener inside bootstrap.php file of your plugin, everything should work fine using the hints posted in the answers. Here is my code (which works properly):
MyPlugin/Config/bootstrap.php:
App::uses('CakeEventManager', 'Event');
App::uses('MyEventListener', 'MyPlugin.Lib/Event');
CakeEventManager::instance()->attach(new MyEventListener());
MyPlugin/Lib/Event/MyEventListener.php:
App::uses('CakeEventListener', 'Event');
class MyEventListener implements CakeEventListener {
...
}
Event listeners related to MyPlugin are being registered only when the plugin is loaded. If I don't want to use the plugin, event listeners are not attached. I think this is a clean solution when you want to add some functionality in various places in your app using a plugin.
Its' not important, where the code resides. Just make sure its being executed and your events are properly registered & attached.
We're using a single file where all events are attached and include it from bootstrap.php, this ensures that all events are available from all locations in the app.
The magic happens when you dispatch an event, like from an controller action.
$event = new CakeEvent('Model.Order.afterPlace', $this, array('some'=>'data') ));
$this->getEventManager()->dispatch($event);
However, you can dispatch events from anywhere you can reach the EventManager (in Models, Controller and Views by default)
Are there callbacks in Laravel like:
afterSave()
beforeSave()
etc
I searched but found nothing. If there are no such things - what is best way to implement it?
Thanks!
The best way to achieve before and after save callbacks in to extend the save() function.
Here's a quick example
class Page extends Eloquent {
public function save(array $options = [])
{
// before save code
parent::save($options);
// after save code
}
}
So now when you save a Page object its save() function get called which includes the parent::save() function;
$page = new Page;
$page->title = 'My Title';
$page->save();
Adding in an example for Laravel 4:
class Page extends Eloquent {
public static function boot()
{
parent::boot();
static::creating(function($page)
{
// do stuff
});
static::updating(function($page)
{
// do stuff
});
}
}
Actually, Laravel has real callback before|after save|update|create some model. check this:
https://github.com/laravel/laravel/blob/3.0/laravel/database/eloquent/model.php#L362
the EventListener like saved and saving are the real callbacks
$this->fire_event('saving');
$this->fire_event('saved');
how can we work with that? just assign it to this eventListener example:
\Laravel\Event::listen('eloquent.saving: User', function($user){
$user->saving();//your event or model function
});
Even though this question has already been marked 'accepted' - I'm adding a new updated answer for Laravel 4.
Beta 4 of Laravel 4 has just introduced hook events for Eloquent save events - so you dont need to extend the core anymore:
Added Model::creating(Closure) and Model::updating(Closure) methods for hooking into Eloquent save events. Thank Phil Sturgeon for finally pressuring me into doing this... :)
In Laravel 5.7, you can create a model observer from the command line like this:
php artisan make:observer ClientObserver --model=Client
Then in your app\AppServiceProvider tell the boot method the model to observe and the class name of the observer.
use App\Client;
use App\Observers\ClientObserver;
class AppServiceProvider extends ServiceProvider
{
public function boot()
{
Client::observe(ClientObserver::class);
}
...
}
Then in your app\Observers\ you should find the observer you created above, in this case ClientObserver, already filled with the created/updated/deleted event hooks for you to fill in with your logic. My ClientObserver:
namespace App\Observers;
use App\Client;
class ClientObserver
{
public function created(Client $client)
{
// do your after-model-creation logic here
}
...
}
I really like the simplicity of this way of doing it. Reference https://laravel.com/docs/5.7/eloquent#events
Your app can break using afarazit solution*
Here's the fixed working version:
NOTE: saving or any other event won't work when you use eloquent outside of laravel, unless you require the events package and boot the events. This solution will work always.
class Page extends Eloquent {
public function save(array $options = [])
{
// before save code
$result = parent::save($options); // returns boolean
// after save code
return $result; // do not ignore it eloquent calculates this value and returns this, not just to ignore
}
}
So now when you save a Page object its save() function get called which includes the parent::save() function;
$page = new Page;
$page->title = 'My Title';
if($page->save()){
echo 'Page saved';
}
afarazit* I tried to edit his answer but didn't work
If you want control over the model itself, you can override the save function and put your code before or after __parent::save().
Otherwise, there is an event fired by each Eloquent model before it saves itself.
There are also two events fired when Eloquent saves a model.
"eloquent.saving: model_name" or "eloquent.saved: model_name".
http://laravel.com/docs/events#listening-to-events