I have built a simple Notification system in my Cake app that I want to have a function that will create a new notification when I call a certain method. Because this is not something the user would actually access directly and is only database logic I have put it in the Notification model like so:
class Notification extends AppModel
{
public $name = 'Notification';
public function createNotification($userId, $content, $url)
{
$this->create();
$this->request->data['Notification']['user_id'] = $userId;
$this->request->data['Notification']['content'] = $content;
$this->request->data['Notification']['url'] = $url;
$result = $this->save($this->request->data);
if ($result)
{
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
}
}
And then whenever I want to create a notification within my app I just do:
$this->Notification->createNotification($userId,'Test','Test');
However this doesn't work! The controller is talking to the model fine, but it doesn't create the row in the database... I'm not sure why... but it would seem I'm doing this wrong by just doing all the code in the model and then calling it across the app.
Edit: Based on answers and comments below, I have tried the following the code to create a protected method in my notifications controller:
protected function _createNotification($userId, $content, $url)
{
$this->Notification->create();
$this->request->data['Notification']['user_id'] = $userId;
$this->request->data['Notification']['content'] = $content;
$this->request->data['Notification']['url'] = $url;
$result = $this->save($this->request->data);
if ($result)
{
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
}
Now the thing that is stumping me still (apologies if this is quite simple to others, but I have not used protected methods in CakePHP before) is how do I then call this from another controller? So for example If have a method in my PostsController and want to create a notification on successful save, how would I do this?
I thought about in my PostsController add method:
if($this->save($this->request-data){
$this->Notification->_createNotification($userId,'Test','Test');
}
But being protected I wouldn't be able to access the method from outside of the NotificationsController. Also I'm using the same syntax as if I was calling a function from a model so again it doesn't feel right.
Hopefully someone can help me out and get me back on track as this is a new area to me.
the controller should pass all data to the model
$this->createNotification($this->request->data);
the model then can use the data:
public function createNotification(array $data) {
$key = $data[$this->alias]['key'];
$data[...] = ...;
$this->create();
return $this->save($data);
}
you never ever try to access the controller (and/or its request object) from within a model.
you can also invoke the method from other models, of course:
public function otherModelsMethod() {
$this->Notification = ClassRegistry::init('Notification');
$data = array(
'Notification' => array(...)
);
$this->Notification->createNotification($data);
}
and you can make your methods verbose, but that usually makes it harder to read/understand/maintain with more and more arguments:
public function createNotification($userId, $content, $url) {
$data = array();
// assign the vars to $data
$data['user_id'] = $userId;
...
$this->create();
return $this->save($data);
}
so this is often not the cake way..
Methods in a model are not "publicly accessible" by definition. A user cannot call or invoke a method in a model. A user can only cause a controller action to be initiated, never anything in the model. If you don't call your model method from any controller, it's never going to be invoked. So forget about the "non-public" part of the question.
Your problem is that you're working in the model as if you were in a controller. There is no request object in a model. You just pass a data array into the model method and save that array. No need for $this->request. Just make a regular array(), put the data that was passed by the controller in there and save it.
The whole approach is totally wrong in the MVC context IMO and screams for the use of the CakePHP event system. Because what you want is in fact trigger some kind of event. Read http://book.cakephp.org/2.0/en/core-libraries/events.html
Trigger an Event and attach a global event listener that will listen for this kind of events and execute whatever it should do (save something to db) when an event happens. It's clean, flexible and extendible.
If you did a proper MVC stack for your app most, if not all, events aka notifications should be fired from within a model like when a post was saved successfully for example.
This is what I have ended up doing. While it certainly isn't glamorous. It works for what I want it to do and is a nice quick win as the notifications are only used in a few methods so I'm not creating a large amount of code that needs improving in the future.
First to create a notification I do the following:
$notificationContent = '<strong>'.$user['User']['username'].'</strong> has requested to be friends with you.';
$notificationUrl = Router::url(array('controller'=>'friends','action'=>'requests'));
$this->Notification->createNotification($friendId,$notificationContent,$notificationUrl);
Here I pass the content I want and the URL where the user can do something, in this case see the friend request they have been notified about. The url can be null if it's an information only notification.
The createNotification function is in the model only and looks like:
public function createNotification($userId, $content, $url = null)
{
$this->saveField('user_id',$userId);
$this->saveField('content',$content);
$this->saveField('url',$url);
$this->saveField('datetime', date('Y-m-d H:i:s'));
$this->saveField('status', 0);
}
This creates a new record in the table with the passed content, sets its status to 0 (which means unread) and the date it was created. The notification is then set as read when a user visits the notifications page.
Again this is most probably not an ideal solution to the problem outlined in this question... but it works and is easy to work with And may prove useful to others who are learning CakePHP who want to run functions from models when building prototype apps.
Remember nothing to stop you improving things in the future!
First of all, you can improve your last solution to do one save() (instead of 5) the following way:
public function createNotification($userId, $content, $url = null){
$data = array(
'user_id' => $userId,
'content' => $content,
'url' => $url,
'datetime' => date('Y-m-d H:i:s'),
'status' => 0
);
$this->create();
$this->save($data);
}
When I began programming CakePHP(1.3) more than a year ago I also had this problem.
(I wanted to use a function of a controller in any other controller.)
Because I didn't know/researched where to place code like this I've done it wrong for over a year in a very big project. Because the project is really really big I decided to leave it that way. This is what i do:
I add a function (without a view, underscored) to the app_controller.php:
class AppController extends Controller {
//........begin of controller..... skipped here
function _doSomething(){
//don't forget to load the used model
$this->loadModel('Notification');
//do ur magic (save or delete or find ;) )
$tadaaa = $this->Notification->find('first');
//return something
return $tadaaa;
}
}
This way you can access the function from your Notification controller and your Posts controller with:
$this->_doSomething();
I use this kind of functions to do things that have nothing to do with data submittance or reading, so i decided to keep them in the app_controller. In my project these functions are used to submit e-mails to users for example.. or post user actions to facebook from different controllers.
Hope I could make someone happy with this ;) but if you're planning to make a lot of these functions, it would be much better to place them in the model!
Related
I'm writing unit tests for an API using PHPUnit and Laravel. Most functions I'm testing require that the user is authenticated before the function can be ran. The user data is stored in one table, and their permissions are stored inside of another table. I can fake the user object inside of Laravel, but I need to be able to also pull the corresponding permissions from the other table without having to hit the database like the dingo router currently is doing.
Currently running Laravel 5.8 and PHPUnit 8.1.5. I currently have the users object that I generated from a Laravel factory saved to a text file. I am able to pass that to a function called "actingAsApi" (found on Github, code below) and that allows me to authenticate as that user. However, the function is still going out and getting all permissions for that user from the database. I'm trying to mock or fake the permissions object it is pulling somewhere so that it doesn't need to hit the database at all. I also tried using the built in Passport functions for Passport::actingAs, and those did not work either as they were still hitting the DB (and not really working anyways).
actingAsApi (inside of TestCase.php)
protected function actingAsApi($user)
{
// mock service middleware
$auth = Mockery::mock('Dingo\Api\Http\Middleware\Auth[handle]',
[
Mockery::mock('Dingo\Api\Routing\Router'),
Mockery::mock('Dingo\Api\Auth\Auth'),
]);
$auth->shouldReceive('handle')
->andReturnUsing(function ($request, \Closure $next) {
return $next($request);
});
$this->app->instance('Dingo\Api\Http\Middleware\Auth', $auth);
$auth = Mockery::mock('Dingo\Api\Auth\Auth[user]',
[
app('Dingo\Api\Routing\Router'),
app('Illuminate\Container\Container'),
[],
]);
$auth->shouldReceive('user')
->andReturnUsing(function () use ($user) {
return $user;
});
$this->app->instance('Dingo\Api\Auth\Auth', $auth);
return $this;
}
Test inside of my Test file
public function testActAs() {
$user = 'tests/users/user1.txt';
$this->actingAsApi($user);
$request = new Request;
$t = new TestController($request);
$test = $t->index($request);
}
I expect the actingAsApi function to allow me to also pass in the mock permissions data that corresponds to my mock user object data from the file, but instead it is hitting the database to pull from the permissions table.
EDIT:
So i've been playing around with doing mock objects, and i figured out how to mock the original controller here:
$controlMock = Mockery::mock('App\Http\Controllers\Controller', [$request])->makePartial();
$controlMock->shouldReceive('userHasPermission')
->with('API_ACCESS')
->andReturn(true);
$this->app->instance('App\Http\Controllers\Controller', $controlMock);
but now I can't figure out how to get my call from the other controllers to hit the mocked controller and not a real one. Here is my code for hitting an example controller:
$info = $this->app->make('App\API\Controllers\InfoController');
print_r($info->getInfo('12345'));
How can i make the second block of code hit the mocked controller and not standup a real one like it does in its constructor method?
Finally came on an answer, and it is now fixed. Here's how I did it for those wondering:
$request = new Request;
$controlMock = m::mock('App\API\Controllers\InfoController', [$request])->makePartial();
$controlMock->shouldReceive('userHasPermission')
->with('API_ACCESS')
->andReturn(true);
print_r($controlMock->getInfo('12345'));
Basically, I was trying to Mock the original API controller, and then catch all of the calls thrown at it. Instead, I should've been mocking the controller I'm testing, in this case the InfoController. I can then catch the call 'userHasPermission', which should reach out to the Controller, but I am automatically returning true. This eliminates the need for hitting the database to receive permissions and other info. More information on how I solved it using Mockery can be found here: http://docs.mockery.io/en/latest/cookbook/big_parent_class.html. As you can see, this is referred to as a 'Big Parent Class'. Good luck!
We are currently working on an application with a Google Login with Laravel with Socialite. We have a Auth user who gets a permission number ex. 264. We have made a function which returns an array with all binary numbers this permission number is made off.
Because calling this function every single time a page loads may be kinda heavy, we thought of adding this once when the Auth::user() is created. We thought of adding a custom constructor in the Model, but we can't make it work.
function __construct($attributes = array()) {
parent::__construct($attributes);
$this->permissionsArray = PermissionHelper::permissionConverter($this->permissions);
}
But we can't get it to work, $this doesn't have values when calling this function.
TLDR;
Directly after making the Auth user I want to call the permissionConverter function and save the data to the user so we can use it more often. Any suggestions on how to do this?
EDIT: I checked all answers out today, succeeded with one of them, but I assumed Laravel put the authenticated user in the SESSION or something. I found out it doesn't and it gets all the data from the database every request. We couldn't do what we requested for unfortunately. So I just had to refactor the script and make it as efficient as possible (although it became a bit less readable for less experienced programmers).
Thanks for the help :D
Maybe you can use this solution ? https://stackoverflow.com/a/25949698/7065748
Create a on the User Eloquent model a boot method with
class User extends BaseModel {
public static function boot() {
static::creating(function($model) {
$model->permissionsArray = PermissionHelper::permissionConverter($model->permissions);
});
// do the same for update (updating) if necessary
}
}
Can't you just use this method ?
If new user:
$user = new User(); // or User:create(['...']) directly
$user->name = 'toto';
// and all other data
or
$user = Auth::user();
then
$user->permissionsArray = PermissionHelper::permissionConverter($user->permissions);
$user->save();
I'm looking to send the user to another page via a controller method. The other page expects POST data.
Normally, the page is accessed with a postLink(). Is there a way to use this in the controller, perhaps with redirect()?
A little bit old but still no answer accepted so...
The answer is no, and yes.
No, there is no direct method since you cannot pass POSTed data using redirect() function.
You could use requestAction(), since you can pass data as posted (see requestAction() here for version cakePHP>=2.0).
In that case you pass an url and then an array having the key data with the posted data, something like
$this->requestAction($url, array('data' =>$this->data));or if you prefer$this->requestAction($url, array('data' =>$this->request->data));
The problem with requestAction() is that the result is environmentally as if you were generating the page of the requested action in the current controller, not in the target, resulting in not very satisfactory effects (at least not usually for me with components behaving not very nicely), so still, no.
...but Yes, you can do something very similar using the Session component.
This is how I usually do it. The flow would be something like this:
View A=>through postLink() to Action in A controller=>=>A controller request->data to Session variable=>=>action in B controller through redirect()=>=>set B controller request->data from Session variable=>=>process data in B controller action=> View B
So, in your A controller, let's say in the sentToNewPage() action you would have something like
//Action in A controller
public function sentToNewPage()
{
$this->Session->write('previousPageInfo', $this->request->data);
$url = array('plugin' => 'your_plugin', 'controller'=>'B',
'action'=>'processFromPreviousPage');
$this->redirect($url);
}
and in B controller:
//Action in B controller
public function beforeFilter()
{//not completelly necessary but handy. You can recover directly the data
//from session in the action
if($this->Session->check('previousPageInfo'))
{$this->data = $this->Session->read('previousPageInfo')};
parent::beforeFilter();
}
public function processFromPreviousPage()
{
//do what ever you want to do. Data will be in $this->data or
// if you like it better in $this->request->data
$this->processUserData($this->request->data);
//...
}
Best solution would be use javascript to redirect.
But if you want more cake I give you some tools
CakeAPI: requestAction - it allow to execute controller method of desire with parameters, if you pass 'return', it will return full view output for that action.
//very useful in views
$result = $this->requestAction('Controller/method',
array('return','user_id'=>$userId)
);
parameter will be accessible in controller via request param
$this->request->params['user_id']
Long and short is that it's not easy to emulate an HTML form with POST data and a redirect, you kind of need to set a bunch of hidden variables containing the data and automatically post the form to your destination via Javascript.
What I would do is take the processing functionality out of the function that requires POST variables, and make it generic so that you can call it from both of your functions.
Consider this rough example:
public function myPostDataAction() {
$name = $_POST['name'];
$age = $_POST['age'];
// do stuff
echo $name . ', ' . $age;
}
Let's say that is the action you are trying to post data to in this scenario, but you can't because you can't emulate those $_POST variables over a redirect without the scenario mentioned at the top here. You can do this:
public function myPostDataAction() {
$name = $_POST['name'];
$age = $_POST['age'];
// call common function
echo $this->getMyResults($name, $age);
}
// accessible from inside the controller only
private function getMyResults($name, $age) {
return $name . ', ' . $age;
}
Now you can also use that getMyResults() functionality by passing regular old variables into it:
public function myProblemFunction() {
$name = 'John';
$age = 15;
echo $this->getMyResults($name, $age);
}
Now, obviously you won't be outputting anything like that straight from the controller action, you'll be setting it to your views etc, but that's an example of how you can centralize functionality to be used in multiple locations.
For the disclaimer, this kind of thing is exactly what models are for, and you should definitely consider putting this kind of function into a model instead of a controller - it depends on your specific application.
With cakephp 2.x Use this:
$this->autoRender = false;
$request = new CakeRequest(Router::url(array('controller' => 'mycontroller','action' => 'my_action')));
$request->data('dataIndex','value');
$response = new CakeResponse();
$d = new Dispatcher();
$d->dispatch(
$request,
$response
);
but it will not redirect but dispatch to a different controller/action so if you went on /controller/oldaction it will stays the current url (no HTTP redirection is done).
You could still change the url with javascript
see: Change the URL in the browser without loading the new page using JavaScript
I have a custom routing class that checks versioning of an object to allow for creation of draft versions of pages that wont appear on the live site. When an admin clicks to preview a draft version of a page my PublishingHelper class on the front-end (called from the routing class) checks the user's permissions to see if they are allowed to view the draft version of this page.
I am using this code:
$context = sfContext::getInstance();
$user = $context->getUser();
But $user is NULL.
Does anyone have any ideas? All my searches seem to say this is the right way of getting the user object.
Thanks,
Tom
Thanks for the comments Till/Jon, Ive managed to fix this now. The factories fix didnt work because while the user class is instantiated none of the filters have run therefore I was left with a useless user object.
I solved my problems simply by taking pretty much all the code in the matchesUrl() function of my custom routing class and putting in a new function doRouting() in the same class. matchesUrl() now looks like this:
public function matchesUrl($url, $context = array())
{
if (false === $parameters = parent::matchesUrl($url, $context))
{
return false;
}
$parameters['module'] = 'content';
$parameters['action'] = 'route';
$this->url = $url;
$this->context = $context;
return $parameters;
}
and the routing is deferred to after the factories and filters by using my "content" module/controller:
class contentActions extends sfActions
{
public function executeRoute(sfWebRequest $request)
{
$router = $this->getRoute();
$router->doRouting($router->url, $router->context);
}
And the doRouting() function now forwards directly to the appropriate module/action (correctly taking into account user permissions).
I think implementing such a restrictions would be easier to implement as a filter: http://www.symfony-project.org/reference/1_4/en/12-Filters
Hi out there in Stackland! Here's my problem:
I want to use my Zend controller to load an array from a database, and then pass it to javascript. I've decided the best way to do this is to use ajax to ask the controller for it's array, encode it in json, and then pass it down. However, I don't know how to pass the variable I loaded in my first action to the action that will pass it down when it gets called via ajax.
The original action which produces the view
public function indexAction()
{
$storeid = $this->getStoreId();
if(!$storeid)
{
$this->_forward('notfound');
return;
}
$store = $this->_helper->loadModel('stores');
$store->getByPrimary($storeid);
}
The action that will be called via ajax
public function getdataAction()
{
$this->_helper->Layout->disableLayout(); // Will not load the layout
$this->_helper->viewRenderer->setNoRender(); //Will not render view
$jsonResponse = json_encode($store);
$this->getResponse()->setHeader('Content-Type', 'application/json')
->setBody($jsonResponse);
}
What I want is to pass $store in indexAction to getdataAction so it can send store as the jsonResponse. Note, these are called at two different times.
Things I have tried that haven't worked:
setting $this->getRequest()->setParam('store', $store) in indexAction, and then using $this->getRequest()->getParam('store'), in getdataAction. I presume this hasn't worked because they're different http requests, so attaching a new param is useless.
using protected $_store in the controller itself, and then saving to it with indexAction, and using it in getdataAction. I'm not really sure why this isn't working.
Is there a good way to pass a variable in this manner? Is there a way to pass a variable between different controllers?(I assume the answer to one is the answer to the other). Could I store it in a controller helper? Do I have to use a session, which I know would work but seems unnecessary? Is there a better way to pass variables to javascript? Am I asking too many questions? Any help would be outstanding. Thanks.
Maybe I'm reading the question wrong, but you should be able to just move $store into the constructor:
public function __construct() {
$store = $this->_helper->loadModel('stores');
$store->getByPrimary($storeid);
}
and have it accessible in all *Action methods. Using sessions seems out of whack for this.
(disclaimer: I'm pretty new to ZF, so I'm interested in other answers found here, and have not tested the below!)
In your view, where you put the ajax call, you will probably address it like:
(See ZF Documentation)
<?= $this->ajaxLink("Example 2",
"/YourController/getdata",
array('update' => '#content',
'class' => 'someLink'),
array('store' => $this->store)); ?>
Notice that in your indexAction, you store the store via:
$this->view->store = $storeid;
Of course, you should note that a web-user could modify the store parameter as it is passed through via an URL.
It would be better architecture to simply add a method to your IndexController, a helper, or somewhere, that returns an instance of Store. Use that method within your indexAction, and your getdataAction (would be more meaningful to call it ajaxAction). Also, you're forgetting to call sendResponse() (remember, you disabled autoRender):
private function indexAction()
{
$this->getStore();
//blah blah
}
private function getStore()
{
$storeid = $this->getStoreId();
if(!$storeid)
{
$this->_forward('notfound');
return;
}
$store = $this->_helper->loadModel('stores');
$store->getByPrimary($storeid);
return $store;
}
public function ajaxAction()
{
$this->_helper->Layout->disableLayout(); // Will not load the layout
$this->_helper->viewRenderer->setNoRender(); //Will not render view
$jsonResponse = json_encode($this->getStore());
$this->getResponse()->setHeader('Content-Type', 'application/json')
->setBody($jsonResponse)
->sendResponse();
}
The manual says:
To send the response output, including
headers, use sendResponse().
http://framework.zend.com/manual/en/zend.controller.response.html
All right, for those of you who want the answer to this too, I just sucked it up and used session. I put a Zend_Session->start() in the bootstrap. I then created a plugin to add a private variable $session to each controller. Then I set $this->session to Zend_Session_Namespace. To pass something, I pass it through session, so I use $this->session->store = $store. I can then pick it up elsewhere with $this->session->store. Thanks to those who tried to help!
Just a quick addition to the comments. To output an array as JSON from within a controller, use:
$array = array('hi' => array('Hello' => 'World');
$this->_helper->json($array);
This sends the response and sets the specific headers for a JSON response