I've configured cakephp 2.6.3 with the events system.
Post data comes in via a controller method and is handled. This data is then passed to an event file as per their documentation
The listener is registering the event as I can write to the log from it. I have a server sent event setup, and I want to pass the resulting data from the listener to this controller method so it can be sent as an event. How do I do this?
Post Data 'Mocked' up event for testing in DataController: - I call this method via a URL to fire the event
public function test_event() {
$data = array(
'lat' => 'lattitude',
'lng' => 'longigiggi',
'msg' => 'my message here'
);
$this->autoRender = false;
$this->layout = false;
$event = new CakeEvent('Data.post.received', $this, array('data', $data));
$this->getEventManager()->dispatch($event);
}
Here is my Event Listener in Lib/Event/TradeData
public function implementedEvents() {
return array(
'Data.post.received' => 'fireThis',
);
}
public function fireThis($event){
CakeLog::write('debug', 'Event listener fired');
//how do I pass data from the event here to a controller method
// to propogate a server sent event in DataController->test_listener();
// The event is being triggered, as its writing to the log
}
DataController::test_listener();
public function test_listener($data = null) {
$trade = new EventController();
$this->getEventManager()->attach($trade);
$this->layout = false;
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
$lat = $data['lat'];
$lng = $data['lng'];
$msg = $data['msg'];
$this->set(compact('lat', 'lng', 'msg'));
$this->render('/Elements/sse');
}
The view Elements/sse is configured correctly for Server Sent Events
<?php
echo "data: {\n";
echo "data: \"lat\": \"$lat\",\n";
echo "data: \"lng\": \"$lng\",\n";
echo "data: \"country\": \"$msg\"\n";
echo "data: }\n\n";
flush();
How do I render data to the same page that the server sent event will be listening to?
First, your Listener and raiser need to be attached somewhere other than in a controller method that you're going to call from the Listener. Maybe you already did this, but in your bootstrap.php file you should have something like:
// Load event listeners
App::uses('TradeData', 'Lib/Event');
App::uses('CakeEventManager', 'Event');
// Attach listeners.
CakeEventManager::instance()->attach(new TradeData());
Then, in the TradeData Listener you can call the controller (subject) method in the fireThis() function:
$event->subject->test_listener($event->data);
Also, you can simplify the data being passed to CakeEvent so it would look like this:
$event = new CakeEvent('Data.post.received', $this, $data);
Related
I need to change something in the user session after it was started. This is temporary, so using an event such as Auth.afterIdentify is what I'm looking for.
What I tried
I have largely referred to this answer on how to even approach this.
I have tried using an anonymous function callback as well as a controller callback method
Following the Auth.afterIdentify documentation I have added implementedEvents
Made sure implementedEvents has a + parent::implementedEvents(), otherwise the controller kept showing me the home page
What I have
Here's my current src/Controller/AppController.php:
<?php
namespace App\Controller;
use Cake\Controller\Controller;
use Cake\Event\Event;
class AppController extends Controller implements \Cake\Event\EventListenerInterface
{
public function initialize()
{
parent::initialize();
// …
$this->loadComponent('Authentication.Authentication');
// Trying with an anonymous function
\Cake\Event\EventManager::instance()->on('Auth.afterIdentify', function ($event) {
Log::write( // noticed when posting this question, should have thrown an error
'info',
'Testing: ' . $event->getSubject()->id
);
debug($event);exit;
});
// Trying a controller callback
\Cake\Event\EventManager::instance()->on('Auth.afterIdentify', [$this, 'afterIdentify']);
}
public function beforeFilter(\Cake\Event\Event $event)
{
parent::beforeFilter($event);
$this->set('myAuth', $this->Authentication->getResult());
$this->set('myUser', $this->Authentication->getIdentity());
}
public function afterIdentify(CakeEvent $cakeEvent, $data, $auth) {
debug([
'$cakeEvent' => $cakeEvent,
'$data' => $data,
'$auth' => $auth,
]);exit;
}
public function implementedEvents()
{
return [
'Auth.afterIdentify' => 'afterIdentify',
] + parent::implementedEvents();
}
}
What doesn't work
It seems neither of the above event listeners is being called. No CakePHP logs are being updated (not even with errors), although they normally work.
What I expected to happen
Calling Log::write without declaring where it comes from should have thrown (and logged) an error
The debug() information was not displayed
Removing the public function afterIdentify method should have caused an error; it didn't – meaning the controller isn't even looking for it
You are mixing up the old auth component and the new authentication plugin, the Auth.afterIdentify event belongs to the former.
The authentication plugin's authentication component has a Authentication.afterIdentify event, but this only applies to authenticators that are stateful and do not implement automatic persisting. So out of the box this only applies to the Form authenticator, and the event is being triggered once on the request where the user was authenticated via the form, on subsequent requests where they are authenticated via for example the Session authenticator, the event is not being triggered.
public function initialize()
{
parent::initialize();
// ...
$this->loadComponent('Authentication.Authentication');
$this->Authentication->getEventManager()->on(
'Authentication.afterIdentify',
function (
\Cake\Event\EventInterface $event,
\Authentication\Authenticator\AuthenticatorInterface $provider,
\Authentication\IdentityInterface $identity,
\Authentication\AuthenticationServiceInterface $service
) {
// ...
$identity['foo'] = 'bar';
$this->Authentication->setIdentity($identity);
}
);
}
I want to render the view to a variable without directly sending it to the browser. I used to do it with cakephp2.*. But, I cannot figure out how to do it in CakePHP3. Could you please tell me how to do it?
ViewBuilder was introduced in CakePHP 3.1 and handles the rendering of views. When ever I want to render to a variable I always go look at how send email works.
From a controller:
function index() {
// you can have view variables.
$data = 'A view variable';
// create a builder (hint: new ViewBuilder() constructor works too)
$builder = $this->viewBuilder();
// configure as needed
$builder->layout('default');
$builder->template('index');
$builder->helpers(['Html']);
// create a view instance
$view = $builder->build(compact('data'));
// render to a variable
$output = $view->render();
}
For Ajax request/response, I use this:
public function print(){
if ($this->request->is('ajax')) {
$data = $this->request->getData();
$builder = $this->viewBuilder()
->setTemplatePath('ControllerName')
->setTemplate('print');
->setLayout('ajax');
->enableAutoLayout(false);
$view = $builder->build(compact('data'));
$html = $view->render();
$res = ['html' => $html];
$this->set('response',$res);
$this->set("_serialize",'response');
}
}
And the print.ctp is under Template/ControllerName
I have an event that I'm creating in my model's beforeSave function:
public function beforeSave(Event $event, Entity $entity) {
$event = new Event('Model.Transaction.createTransaction', $this, [
'transaction' => $entity
]);
$this->eventManager()->dispatch($event);
if ($event->isStopped()) {
return FALSE;
}
}
Which is grabbed by a Listener I have created:
public function implementedEvents() {
return [
'Model.Transaction.createTransaction' => 'createHistorical'
];
}
This all works fine. What I was wondering is if there's any way to pass a message back to the model where the event was registered. So that if there's a failure or something, I can pass a message back to the model and add it to the errors to be displayed on the form?
You can return a value from the callback method (createHistorical in your case) which would be accessible using $event->result after dispatching is done.
I am trying to invalidate a field by a condition in controller instead of Model.
$this->Model->invalidate('check_out_reason', __('Please specify check out reason.', true));
The above won't work to invalidate the field. Instead, I need the below:
$this->Model->invalidate('Model.check_out_reason', __('Please specify check out reason.', true));
However, if I wish get the error message show up in the "field" itself ($this->model->validationErrors), it needs to be "check_out_reason" instead of "Model.check_out_reason". That means, I can't get the error message to show up in the field itself if I wish to invalidate the input in controller.
May I know is this a bug in CakePHP?
i created a test controller called "Invoices", just for testing, and i developed the following function
public function index(){
if (!empty($this->request->data)) {
$this->Invoice->invalidate('nombre', __('Please specify check out reason.'));
if ($this->Invoice->validates()) {
// it validated logic
if($this->Invoice->save($this->request->data)){
# everthing ok
} else {
# not saved
}
} else {
// didn't validate logic
$errors = $this->Invoice->validationErrors;
}
}
}
i think it worked for me
Change the field "nombre" for your field called "check_out_reason" to adapt the function to your code
I found a workaround for manual invalidates from controller. Reading a lot on this issue I found out that the save() function doesn't take in consideration the invalidations set through invalidate() function called in controller, but (this is very important) if it is called directly from the model function beforeValidate() it's working perfectly.
So I recommend to go in AppModel.php file and create next public methods:
public $invalidatesFromController = array();
public function beforeValidate($options = array()) {
foreach($this->invalidatesFromController as $item){
$this->invalidate($item['fieldName'], $item['errorMessage'], true);
}
return parent::beforeValidate($options);
}
public function invalidateField($fieldName, $errorMessage){
$this->invalidatesFromController[] = array(
'fieldName' => $fieldName,
'errorMessage' => $errorMessage
);
}
After that, make sure that your model's beforeValidate() function calls the parent's one:
public function beforeValidate($options = array()) {
return parent::beforeValidate($options);
}
In your controller for invalidating a field use next line:
$this->MyModel->invalidateField('fieldName', "error message");
Hope it helps! For me it's working!
I'm trying to log every write operation so I'm using the afterSave and afterDelete callbacks in AppModel. Basically I need to log(for the moment): the model , the controller function, the loggedin user data and the remote ip
It seems that I was able to get all of them but I don't know how to get the controller function name.
This is the aftersave function I have now:
public function afterSave($created) {
App::uses('Folder', 'Utility');
$month = date("y-m");
if(!is_dir('../tmp/logs/'.$month)) {
$dir = new Folder('../tmp/logs/'.$month, true);
}
App::uses('CakeSession', 'Model/Datasource');
$user_id = CakeSession::read('Auth.User.username');
if($created) {
$id = 'New';
} else {
$id = $this->data[$this->alias]['id'];
}
$str = 'WRITE Action. Model: '.$this->alias.'. Controller: functon_name. ID:'.$id.'. Username: '.$user_id.'. Client IP: '.$this->getIP();
CakeLog::write($month.'/'.date("d-m-y"), $str);
}
Thanks
You're doing this on the model, which has no knowledge of the controller (and really shouldn't). I'd suggest copying the CakeRequest object to the model so you have that information. Here's one way:
//controller
function beforeFilter() {
$this->{$this->modelClass}->request = $this->request;
}
Then you can access the request object from the model. Use $this->request['params']['action'] to get the current dispatched action.
It's worth suggesting that you might want to move this to the read() method on a custom datasource, as afterSave() can possibly be skipped and therefore not logged.