Cakephp 2.3 beforeFilter and implementedEvents aren't able to co-exist - cakephp

Using cakephp 2.3.5.
I use beforeFilter in several controllers to allow certain actions without the need to log in. I've used it successfully for quite some time. However, I've noticed that I can't get beforeFilter to fire if the controller also has the implementedEvents() method.
public function beforeFilter() {
parent::beforeFilter();
$this->Auth->allow('matchWwiProducts');
}
public function implementedEvents() {
return array(
'Controller.Product.delete' => 'deleteSku',
'Controller.Product.price' => 'notifySubscribers',
'Controller.Product.stock' => 'notifySubscribers'
);
}
For the code as displayed above I will be forced to do a login if I call the method www.example.com/products/matchWwiProducts.
When I comment out the implementedEvents() everything works as intended. I've searched around and can't find any references to implementedEvents() creating issues with beforeFilter.
The action matchWwiProducts() is as follows. It works perfectly when I log in. However, I don't want to force a log in for this action to take place.
public function matchWwiProducts() {
// this is an audit function that matches Products sourced by wwi
//
$this->autoRender = false; // no view to be rendered
// retrieve products sourced from wwi from Table::Product
$this->Product->contain();
$wwiProducts = $this->Product->getWwiSkus();
$wwiProductCount = count($wwiProducts);
// retrieve products sourced from wwi from Table:Wwiproduct
$this->loadModel('WwiProduct');
$this->Wwiproduct->contain();
$wwiSource = $this->Wwiproduct->getSkuList();
$wwiSourceCount = count($wwiSource);
// identify SKUs in $wwiProducts that are not in $wwiSource
$invalidSkus = array_diff($wwiProducts, $wwiSource);
// identify SKUs in $wwiSource that are not in $wwiProducts
$missingSkus = array_diff($wwiSource, $wwiProducts);
$missingSkuDetails = array();
foreach ($missingSkus as $missingSku) {
$skuStockStatus = $this->Wwiproduct->getStockStatus($missingSku);
$missingSkuDetails[$missingSku] = $skuStockStatus;
}
$email = new CakeEmail();
$email->config('sourcewwi');
$email->template('sourcewwiaudit', 'sourcewwi');
if (count($invalidSkus) > 0 || count($missingSkus) > 0) {
$email->subject('WWI Source Audit: Invalid or Missing SKUs');
$email->viewVars(array('invalidSkus' => $invalidSkus,
'missingSkuDetails' => $missingSkuDetails,
'wwiProductCount' => $wwiProductCount,
'wwiSourceCount' => $wwiSourceCount));
} else {
$email->subject('WWI Source Audit: No Exceptions');
$email->viewVars(array('wwiProductCount' => $wwiProductCount,
'wwiSourceCount' => $wwiSourceCount));
}
$email->send();
}

It doesn't fire because you're overloading the implementendEvents() method without making sure you keep the existing events there.
public function implementedEvents() {
return array_merge(parent::implementedEvents(), array(
'Controller.Product.delete' => 'deleteSku',
'Controller.Product.price' => 'notifySubscribers',
'Controller.Product.stock' => 'notifySubscribers'
));
}
Overloading in php.
Check most of the base classes, Controller, Table, Behavior, Component, they all fire or listen to events. So be careful when extending certain methods there. Most simple way might to do a search for "EventListenerInterface" in all classes. A class that implements this interface is likely to implement event callbacks.

Related

Process post action with another action within controller

I have a terribly designed app where the index action displays a form in a JavaScript-based dialogue that's submitted to process action and then redirects to index (either on success or on error). The process action does not even have a view:
class UnicornsController
{
public function index($foo, $bar)
{
$this->set(
array(
'unicorn' => $this->Unicorn->findByFooAndBar($foo, $bar);
)
);
}
public function process()
{
$this->Unicorn->save($this->request->data);
$this->redirect(
array(
'action' => 'index',
$this->request->data['Unicorn']['foo'],
$this->request->data['Unicorn']['bar'],
)
);
}
}
I'm adding proper error reporting. I'm trying to change the this->redirect() part so $this->request->data is not lost and I have a chance to display it again in the form generated in index.ctp but I can't get it right: both $this->requestAction() and $this->index() try to render process.ctp anyway. Am I using them incorrectly or I'm missing the right approach?
If you want to run a different action, you can use Controller::setAction(), it changes the action parameter in the request, sets the template to render accordingly, and returns the possible return value of the invoked action.
public function process()
{
// ....
$this->setAction(
'index',
$this->request->data['Unicorn']['foo'],
$this->request->data['Unicorn']['bar']
);
}
See also
API > Controller::setAction()

CakePHP 3 - implementedEvents() - does not fire implemented event

Trying to start off with CakePHP's EventListener. I have set an event up, but it is not firing. I can't figure out why? This is the code I have so far...
public function view($slug = null) {
$profile = $this->Profiles->find()->where(['slug' => $slug])->first();
$this->set('profile', $profile);
$this->set('_serialize', ['profile']);
}
// I have confirmed this works.. but it is not calling the updateCountEvent method
public function implementedEvents(){
$_events = parent::implementedEvents();
$events['Controller.afterView'] = 'updateCountEvent';
return array_merge($_events, $events);
}
/**
* Triggered when a profile is viewed...
*/
public function updateCountEvent(){
Log::write('error', "Update count events"); // I dont get this line in the log. Not sure why this does not fire...
}
I revisited this question and was able to come up with a solution that works for me. Thanks Jose Lorenzo for the 'heads up'. This is my solution:
use Cake\Event\Event;
public function view($slug = null) {
$profile = $this->Profiles->find()->where(['slug' => $slug])->first();
$this->profileId = $profile->id;
$event = new Event('Controller.Profiles.afterView', $this);
$this->eventManager()->dispatch($event);
$this->set('title', $profile->name);
$this->set('profile', $profile);
$this->set('_serialize', ['profile']);
}
public function implementedEvents(){
$_events = parent::implementedEvents();
$events['Controller.Profiles.afterView'] = 'updateCountEvent';
return array_merge($_events, $events);
}
public function updateCountEvent(){
$profile = $this->Profiles->get($this->profileId);
$profile->set('views_count', $profile->views_count + 1);
$this->Profiles->save($profile);
}
I see the power of Events especially if I get to send out emails, update more tables and perhaps run a cron..., however instead of writing these 2 lines of code and create 2 more methods for this specific case, I could have made this simple call within the view action as such
$profile->set('views_count', $profile->views_count + 1);
$this->Profiles->save($profile);
The question would be.... Should I have opted for this simpler process, or stick with the events route?

CakePHP 2.1 HTTP cache

I'm trying to speed up my site by taking advantage of the new HTTP cache features in CakePHP 2.1:
class ArticlesController extends AppController {
public function view($id) {
$article = $this->Article->find(
'first',
array('conditions' => array('Article.id' => $id))
);
$this->response->modified($article['Article']['modified']);
$this->set(compact('article'));
}
}
Caching works fine, but does not distinguish between different users (i.e. if a user logs in and visits a page that was already cached, the previously cached page is displayed, and user-specific content is not shown). I'd like one of the following to happen:
Cache discriminates between different users and stores a separate cache for each user
Caching is disabled if a user is logged in (the user login is only used for admin purposes)
I've tried adding
if (AuthComponent::user('id')) {
$this->disableCache();
}
But this doesn't seem to solve the problem
Does anyone know how to get this to work, or am I doing something fundamentally wrong?
You could try the etag caching method and generate a hash based on the article id and user id.
See http://book.cakephp.org/2.0/en/controllers/request-response.html#the-etag-header
The Etag header (called entity tag) is string that uniquely identifies the requested resource. It is very much like the checksum of a file, caching will compare checksums to tell whether they match or not.
To actually get advantage of using this header you have to either call manually CakeResponse::checkNotModified() method or have the RequestHandlerComponent included in your controller:
<?php
public function index() {
$articles = $this->Article->find('all');
$this->response->etag($this->Article->generateHash($articles));
if ($this->response->checkNotModified($this->request)) {
return $this->response;
}
...
}
I thought I'd post the solution(s) I eventually used, in case it helps anyone.
To disable caching completely for logged in users:
class ArticlesController extends AppController {
public function view($id) {
$article = $this->Article->find(
'first',
array('conditions' => array('Article.id' => $id))
);
if (!AuthComponent::user('id')) {
$this->response->etag($this->Article->generateHash($article));
}
$this->set(compact('article'));
}
}
To have a separate cache for each user (and for the case when no-one is logged in):
class Article extends AppModel {
public function generateHash($article) {
if (AuthComponent::user('id')) {
return md5(AuthComponent::user('id') . '-' . $article['Article']['modified']);
} else {
return md5($article['Article']['modified']);
}
}
}
class ArticlesController extends AppController {
public function view($id) {
$article = $this->Article->find(
'first',
array('conditions' => array('Article.id' => $id))
);
$this->response->etag($this->Article->generateHash($article));
$this->set(compact('article'));
}
}

CakePHP: How to list only the Posts which have been posted by the user himself/herself (one user)

On this page, a user would see a list of links which he himself/herself has posted, not including the rests which were posted by others. I'm in the middle of writing the function in one of my models, e.g. news
I am trying to use the idea of a dashboard: http://nuts-and-bolts-of-cakephp.com/2008/12/16/how-to-build-a-dashboard-for-your-application-in-cakephp/
I created my dashboards controller as:
function index () {
$this->set('news', ClassRegistry::init('News')->showmy());
}
// In my news::model I have a function called showmy()
function showmy() {
$userid = $this->Session->read('Auth.User.id');
$mynews = $this->News->find('all', array('conditions' => array('News.user_id' => '$userid')));
$this->set('mynews', $mynews);
}
//The error I get is as follow
Undefined property: News::$Session [APP\models\news.php, line 7]
Fatal error: Call to a member function read() on a non-object in C:\...\app\models\news.php on line 7
I know something is terribly wrong with the showmy function, can someone shed some light how do we write the function that only retrieves the posts by one user? or if the problems are minor, correct the function above?
Try this
You can't use Session from model so passit from controller
function index () {
$userid = $this->Session->read('Auth.User.id');
$this->set('news', ClassRegistry::init('News')->showmy($userid ));
}
function showmy($userid) {
return $this->find('all', array('conditions' => array('News.user_id' => $userid)));
}
My approach seems to be more documentation-like:
Let say you have PostsController with Post Model. When you do index on posts it's fine, you query for all posts. But for one user, I think you mean single index action in PostsController like:
class Post extends AppModel {
public $belongsTo = 'User'; // This way you're telling cakephp that one user can have many posts (he's posts' author)
}
class PostsController extends AppController {
public function viewByUser() {
$id = $this->Auth->user('id');
$posts = $this->Post->findAllById($id);
$this->set('posts', $posts);
}
}
And then, in your view, you build a table like for index action
http://book.cakephp.org/2.0/en/models/retrieving-your-data.html#magic-find-types

Cakephp Auth with multiple "Users" tables

I would like to know how to deal with only ONE authentification process and "users" in multiple tables. I have 4 Users table: users, admins, artists, teamadmins which all have specific fields, but I would like all of these users to be able to connect via only one form on the homepage, and being redirected after that to their specific dashboards.
I think the redirections shouldn't be a problem, and some routes added should work, but I really don't know where to look/start to ake this all possible.
Cheers,
Nicolas.
EDIT: here's the final solution (thanks to deizel)
App::import('Component', 'Auth');
class SiteAuthComponent extends AuthComponent {
function identify($user = null, $conditions = null) {
$models = array('User', 'Admin', 'Artist');
foreach ($models as $model) {
$this->userModel = $model; // switch model
$this->params["data"][$model] = $this->params["data"]["User"]; // switch model in params/data too
$result = parent::identify($this->params["data"][$model], $conditions); // let cake do its thing
if ($result) {
return $result; // login success
}
}
return null; // login failure
}
}
CakePHP's AuthComponent only supports authentication against a single "User" model at a time. The model is chosen by setting the Auth::userModel property, but it only accepts a string and not an array of models.
You can switch the userModel on the fly with the following code, but this requires you to know in advance which model to switch to (eg. your users have to choose their account type from a dropdown):
public function beforeFilter() {
if (isset($this->data['User']['model'])) {
$this->Auth->userModel = $this->data['User']['model'];
}
}
You can likely extend the core AuthComponent to add the functionality you want by overwriting the AuthComponent::identify() method so it loops over and attempts authentication with each model:
App::import('Component', 'AuthComponent');
class AppAuthComponent extends AuthComponent {
function identify($user = null, $conditions = null) {
$models = array('User', 'Admin', 'Artist', 'TeamAdmin');
foreach ($models as $model) {
$this->userModel = $model; // switch model
$result = parent::identify($user, $conditions); // let cake do it's thing
if ($result) {
return $result; // login success
}
}
return null; // login failure
}
}
You will have to replace occurrences of Auth in your application with AppAuth to use your extended AuthComponent, unless you use this trick.
While annoying, I think the best solution is probably using Cake's built in ACL support (see http://book.cakephp.org/2.0/en/tutorials-and-examples/simple-acl-controlled-application/simple-acl-controlled-application.html).
If you do authentication the way you're talking about, you have to keep track of permissions in your controller code, checking to see what the userModel is. If you use an access control list, the permission tree will already exist in the database, which should simplify your code a great deal, and make it more modular.
It also means restructuring your data model to have a single users table and groups table instead of entity classes for each type of user.
I just went through the process of doing this myself... :(
this is also a possibility
public function beforeFilter() {
parent::beforeFilter();
$this->Auth->authenticate = array(
AuthComponent::ALL => array('userModel' => 'AnotherModel'),
'Form',
'Basic'
);
}
Here is the final solution as suggested by deizel and modified by Nicolas:
App::import('Component', 'Auth');
class SiteAuthComponent extends AuthComponent {
function identify($user = null, $conditions = null) {
$models = array('User', 'Admin', 'Artist');
foreach ($models as $model) {
$this->userModel = $model; // switch model
$this->params["data"][$model] = $this->params["data"]["User"]; // switch model in params/data too
$result = parent::identify($this->params["data"][$model], $conditions); // let cake do its thing
if ($result) {
return $result; // login success
}
}
return null; // login failure
}
}

Resources