I am impressed by the flexibility of Yii events. I am new to Yii and I want to know how to pass parameters to Yii event handlers?
//i have onLogin event defined in the login model
public function onLogin($event)
{
$this->raiseEvent("onLogin", $event);
}
I have a login handler defined in the handler class. This event handler method takes a parameter:
function loginHandler($param1)
{
...
}
But here, I am confused as to how to pass a parameter to the login event handler:
//i attach login handler like this.
$loginModel->onLogin = array("handlers", "loginHandler");
$e = new CEvent($this);
$loginModel->onLogin($e);
Am I doing something wrong? Is there another approach for this?
Now I have an answer for my own question. CEvent class has a public property called params where we can add additional data while passing it to event handler.
//while creating new instance of an event I could do this
$params = array("name" => "My Name");
$e = new CEvent($this, $params);
$loginModel->onLogin($e);
//adding second parameter will allow me to add data which can be accessed in
// event handler function. loginHandler in this case.
function loginHandler($event)// now I have an event parameter.
{
echo $event->params['name']; //will print : "My Name"
}
If you want to use onBeginRequest and onEndRequest you can do it by adding the next lines into your config file:
return array (
'onBeginRequest'=>array('Y', 'getStats'),
'onEndRequest'=>array('Y', 'writeStats'),
)
or you can do it inline
Yii::app()->onBeginRequest= array('Y', 'getStats');
Yii::app()->onEndRequest= array('Y', 'writeStats');`class Y {
public function getStats ($event) {
// Here you put all needed code to start stats collection
}
public function writeStats ($event) {
// Here you put all needed code to save collected stats
}
}`
So on every request both methods will run automatically. Of course you can think "why not simply overload onBeginRequest method?" but first of all events allow you to not extend class to run some repeated code and also they allow you to execute different methods of different classes declared in different places. So you can add
Yii::app()->onEndRequest= array('YClass', 'someMethod');
at any other part of your application along with previous event handlers and you will get run both Y->writeStats and YClass->someMethod after request processing. This with behaviors allows you create extension components of almost any complexity without changing source code and without extension of base classes of Yii.
Related
Trying to learn events in Yii 2. I found a few resources. The link I got more attention is here.
How to use events in yii2?
In the first comment itself he explains with an example. Say for an instance we have 10 things to do after registration - events comes handy in that situation.
Calling that function is a big deal? The same thing is happening inside the model init method:
$this->on(self::EVENT_NEW_USER, [$this, 'sendMail']);
$this->on(self::EVENT_NEW_USER, [$this, 'notification']);
My question is what is the point of using events? How should I get full benefit of using them. Please note this question is purely a part of learning Yii 2. Please explain with an example. Thanks in advance.
I use triggering events for written (by default) events like before validation or before deletion. Here's an example why such things are good.
Imagine that you have some users. And some users (administrators, for example) can edit other users. But you want to make sure that specific rules are being followed (let's take this: Only main administrator can create new users and main administrator cannot be deleted). Then what you can do is use these written default events.
In User model (assuming User models holds all users) you can write init() and all additional methods you have defined in init():
public function init()
{
$this->on(self::EVENT_BEFORE_DELETE, [$this, 'deletionProcess']);
$this->on(self::EVENT_BEFORE_INSERT, [$this, 'insertionProcess']);
parent::init();
}
public function deletionProcess()
{
// Operations that are handled before deleting user, for example:
if ($this->id == 1) {
throw new HttpException('You cannot delete main administrator!');
}
}
public function insertionProcess()
{
// Operations that are handled before inserting new row, for example:
if (Yii::$app->user->identity->id != 1) {
throw new HttpException('Only the main administrator can create new users!');
}
}
Constants like self::EVENT_BEFORE_DELETE are already defined and, as the name suggests, this one is triggered before deleting a row.
Now in any controller we can write an example that triggers both events:
public function actionIndex()
{
$model = new User();
$model->scenario = User::SCENARIO_INSERT;
$model->name = "Paul";
$model->save(); // `EVENT_BEFORE_INSERT` will be triggered
$model2 = User::findOne(2);
$model2->delete(); // `EVENT_BEFORE_DELETE` will be trigerred
// Something else
}
Events in Yii looks great, but several questions still wakes me at night:
If I raise an event and create several PHP event handler classes in chain, can I pass different data between them (like return value)?
Is the event designed for this goal? As far as I see, the event seems to be one-direction way of notification and passing data back is not a common practice, is that correct?
Lets say:
I have 3 handlers : Handler1, Handler2, Handler3 executed in this order. Each Handler concatenates some string data.
Can I pass the concatenated sting between handlers and are the handlers assumed to do this?
In a event chain, is throwing an exception in an event handler a good practice?
You're correct that the event system was primarily designed (or at least: documented) as a read-only notification system. However, it is possible to do what you want by creating your own subclassed Event that defines a public property for the data you want to pass around.
For example, start with a custom event class:
class MyEvent extends \yii\base\Event
{
public $data;
}
Trigger this event:
$event = new MyEvent([
'data' => 'hello world'
]);
$this->trigger('myEvent', $event);
echo "After passing through the entire event chain, data is now: " . $event->data;
And add behaviors (or handlers) that listen to it:
public function onMyEvent($event)
{
$event->data .= ', goodbye world';
}
If all went well, this should end up echo'ing hello world, goodbye world
I have a bit of an issue, the below code is from one of the methods within my controller that I'm testing.
The scenario is, you save a record and you're automatically directed to 'viewing' that record. So I am passing in the items id upon save to the redirect...
However, when running the tests I receive 'ErrorException: Trying to get property of non-object' if I pass in the id of the object straight off. So the work around I'm doing the pass the test is a ternary condition to see if the output is an object...surely there must be a better way of doing this?
I'm using Mockery, and have created a mock class/interface for the Projects model which is injected into the Projects main controller.
Here's the method:
public function store()
{
// Required to use Laravels 'Input' class to catch the form data
// This is because the mock tests don't pick up ordinary $_POST
$project = $this->project->create(Input::only('projects'));
if (count(Input::only('contributers')['contributers']) > 0) {
$output = Contributer::insert(Input::only('contributers')['contributers']);
}
// Checking whether the output is an object, as tests fail as the object isn't instatiated
// through the mock within the tests
return Redirect::route('projects.show', (is_object($project)?$project->id:null))
->with('fash', 'New project has been created');
}
And heres the test which is testing the redirected route.
Input::replace($input = ['title' => 'Foo Title']);
$this->mock->shouldReceive('create')->once();
$this->call('POST', 'projects');
$this->assertRedirectedToRoute('projects.show');
$this->assertSessionHas('flash');
You have to define the response of your mock when the method create is called to properly simulate the real behavior :
$mockProject = new StdClass; // or a new mock object
$mockProject->id = 1;
$this->mock->shouldReceive('create')->once()->andReturn($mockProject);
I have an extension for product registration that dispatches an event after the registration is saved. Another extension uses that event to generate a coupon for a virtual product if it is related to the registered product.
I need to get back data on the generated coupon to send to the user in an email along with the details of their product registration.
Is there a way to return data from the observer back to where the event is dispatched?
There is a trick available in Magento for your purpose. Since you can pass event data to the observers, like product or category model, it also possible to create a container from which you can get this data.
For instance such actions can be performed in dispatcher:
$couponContainer = new Varien_Object();
Mage::dispatchEvent('event_name', array('coupon_container' => $couponContainer));
if ($couponContainer->getCode()) {
// If some data was set by observer...
}
And an observer method can look like the following:
public function observerName(Varien_Event_Observer $observer)
{
$couponContainer = $observer->getEvent()->getCouponContainer();
$couponContainer->setCode('some_coupon_code');
}
Enjoy and have fun!
No, there's nothing built in to the system for doing this. The Magento convention is to create a stdClass or Varien_Object transport object.
Take a look at the block event code
#File: app/code/core/Mage/Core/Block/Abstract.php
...
if (self::$_transportObject === null)
{
self::$_transportObject = new Varien_Object;
}
self::$_transportObject->setHtml($html);
Mage::dispatchEvent('core_block_abstract_to_html_after',
array('block' => $this, 'transport' => self::$_transportObject));
$html = self::$_transportObject->getHtml();
...
Since self::$_transportObject is an object, and PHP objects behave in a reference like manner, any changes made to the transport object in an observer will be maintained. So, in the above example, if an observer developer said
$html = $observer->getTransport()-setHtml('<p>New Block HTML');
Back up in the system block code self::$_transportObject would contain the new HTML. Keep in mind that multiple observers will have a chance to change this value, and the order observers fire in Magento will be different for each configured system.
A second approach you could take is to use Magento's registry pattern. Register a variable before the dispatchEvent
I'm having an issue with preserving the value of a variable after an HMVC sub-request in Kohana 3.1.3.1 and am wondering how best to approach/fix it. I thought that additional requests in Kohana were isolated from each other, but it doesn't seem to be the case...
First of all, I've created a controller to extend the Controller_Template:
abstract class Controller_Website extends Controller_Template {
public $page_info;
public $allow_caching;
public function before()
{
// ... more unrelated code here ...
// Only do this if auto_render is TRUE (default)
if ($this->auto_render === TRUE AND $this->request->is_initial())
{
// Set our Page_Info to the values we just loaded from the database
$this->page_info = clone $this->navs[$slug];
}
// ... more unrelated code here ...
}
public function after()
{
// ... more unrelated code here ...
// For internal requests, let's just get everything except for the template
if (! $this->request->is_initial())
{
$this->response->body($this->template->main_view->render());
}
// Only if auto_render is still TRUE (Default)
if ($this->auto_render === TRUE AND $this->request->is_initial())
{
// ... more unrelated code here ...
// ... get stuff from the database to populate the template ...
// now render the response body
$this->response->body($this->template->render());
}
// ... more unrelated code here...
// including setting headers to disable/enable caching
}
}
And here's an example of what one of the controllers looks like:
class Controller_Events extends Controller_Website {
public function action_filtered()
{
// ... do some stuff ...
// and set some values
$this->page_info->page_title = 'Set in Events controller';
// and some more stuff including generating some output
}
}
Now I want one of my other controllers to be able to pull the output from the events controller, without the template. Controller_Website (above) takes care of excluding the template from the output, but consider this:
class Controller_Search extends Controller_Website {
public function action_index()
{
// ... do some stuff ...
// now let's include the result from our events controller
$this->template->main_view->events = Request::factory()
->controller('events')
->action('filtered')
->execute();
// and set some values
$this->page_info->page_title = 'Set in Search controller';
// and some more stuff including generating some output
}
}
So when my template calls echo $this->page_info->page_title; (remember, my template is only being included in the search controller's output and not the event controller's output), I'm expecting it to return "Set in Search controller" but instead it returns "Set in Events Controller"
The problem is that this action_filtered() method is very long and I've set up a couple routes that use this method to output several event pages (like filtering events by year, month, venue, city, etc.) so it doesn't make sense to duplicate this method in my search controller. Hence the need for an HMVC request. When the filtered action is called as a main/initial request, it makes sense to set values in $page_info but when it's called as a sub-request, I need to preserve the values set in the search controller, or whatever the initial controller is.
Of course, I could create an if statement in the events controller to only update these values if it's a main request, but that's less than ideal, obviously. There must be a way to make this sub-request run isolated from the initial request?
What am I doing wrong, or what's the best way to go about solving this?
Thanks in advance!
DM
Request::factory()
->controller('events')
->action('filtered')
->execute();
Its incorrect. Request::factory() call returns an initial Request instance (so it uses current URI value). You need a generated URI for your HMVC call:
Request::factory(Request::current()->uri(array(
'controller' => 'events',
'action' => 'filtered'
)))->execute();
PS. Sorry, its my mistake. Your code seems to be valid in 3.1. Anyway, try to change it with Request->uri() usage.
I found the problem/solution!
The problem is that in my Controller_Website I had this in my action_before():
// Create a new DM_Nav object to hold our page info
// Make this page info available to all views as well
$this->page_info = new DM_Nav;
View::bind_global('page_info', $this->page_info);
The problem is the bind_global – which is actually working as it's supposed to by letting you change the value of this variable after the fact... (A very neat feature indeed.)
The workaround/solution was to force the template to use only the original page_info by detecting if it was an initial/main request. So at the very end of the action_before() method of Controller_Website where it says:
// Only if auto_render is still TRUE (Default)
if ($this->auto_render === TRUE AND $this->request->is_initial())
{
I added this line to the end of that if statement:
$this->template->page_info = $this->page_info;
}
This line is redundant on initial/main requests, but it means that any additional sub-requests can still have access to their own page_info values without affecting the values used in the template. It appears, then, that if you assign a property to a view and then attempt to bind_global() the same property with new values, it doesn't overwrite it and uses the original values instead... (Which is why this solution works.) Interesting.