How do I load associated models and save related data in CakePHP? - cakephp

I am setting up a user/group system that allows users to send requests to join a group.
I can't seem to load the associated model and add a row. It's really really really difficult to resist the urge to just use $this->query() and be done with it... but I'm trying to learn the Cake conventions and do things the right way.
In my group model's function for handling group join requests:
$this->loadModel('GroupRequest');
$this->data = array('GroupRequest' =>
array('user_id' => $uid, 'group_id' => $gid));
if($this->GroupRequest->save($this->data)){ echo 'save success'; }
else { echo 'save fail'; }
Here are the errors I get when I run this:
Warning (512): SQL Error: 1064: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'loadModel' at line 1 [CORE/cake/libs/model/datasources/dbo_source.php, line 684]
Query: loadModel
Notice (8): Undefined property: Group::$GroupRequest [APP/models/group.php, line 124]
Fatal error: Call to a member function save() on a non-object in /home/wxworksmat/sspot3/app/models/group.php on line 124
I also tried using App::import:
App::import('Model','GroupRequest');
I don't get any SQL errors importing the Model this way, but it still doesn't work. I get the following error on the save() or create() call:
Fatal error: Call to a member function save() on a non-object in /home/wxworksmat/sspot3/app/models/group.php on line 124

You are confusing controller and model methods
$this->loadModel()
is a controller method and can only be used there.
You should always use
$this->ModelName = ClassRegistry::init('ModelName');
everywhere else

I might be wrong, and please excuse me if I'm wrong, but it looks like you don't understand the concept of the framework very well. It is difficult to answer your question without giving you a complete tutorial.
This said, everything relies on model associations. If it's done correctly, things are getting easy. You should read:
Associations: Linking Models Together
Once you have your models correctly linked, you will be able to save the primary model, as well as the related model, very easily.
Saving Related Model Data (hasOne, hasMany, belongsTo)

As I understand, you are trying to do this from inside a model?
class GroupRequest extends AppModel {
public function associate($user, $group) {
$data["GroupRequest"] = array("user_id" => $user, "group_id" => $group);
$this->save($data);
}
}
Then in your Controller (assuming group_requests_controller)
$this->GroupRequest->associate($user, $group);
If you're calling this from another controller you would loadModel first
$this->loadModel("GroupRequests");
$this->GroupRequest->associate($user, $group);
However, if you're doing all of this from within GroupRequests controller you should be able to save directly, without making a separate method for it
public function add() {
$this->GroupRequest->create();
$this->GroupRequest->save($this->data); #for < 2.0
}
Your view should be something like
<?php
echo $this->Form->create("GroupRequest");
echo $this->Form->input("user_id");
echo $this->Form->input("group_id");
echo $this->Form->end("Submit");
?>

The problem I had was that I didn't have the correct model association declarations at the top of my model.
Now I have:
group.php
var $hasMany = 'GroupRequest';
group_request.php
var $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Group' => array(
'className' => 'Group',
'foreignKey' => 'group_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
);
public function new_request($user, $group) {
$data["GroupRequest"] = array("user_id" => $user, "group_id" => $group, 'status' => 'pending');
if($this->save($data)){ return true;} else {return false;}
}
Now because everything is set up CORRECTLY... I can do this in my group.php model:
$this->GroupRequest->new_request($uid,$gid)
As an added bonus, because the assocations are populating properly, when I do $this->find in my group or user model, now all the related GroupRequest entries show up. Bonus data FTW.

Related

CakePHP 3: search form with get method and find condition, how to prevent SQL injection?

I have an HTML form with the GET method, and five text input field, which should help to filter the data. When users fill one or more fields, these data are shown as url query.
My question is how to safely use the this query data without the possibility of SQL injection?
EDIT
Of course, is a simple filtering of user data, by name, location, etc., not fulltext search.
'first_name LIKE' => '%'.$this->request->query('first_name').'%'
Where is in the documentation explained bind method, like ?
->bind(':name', $this->request->query('name'))
To avoid SQL injection vulnerabilities, you can use query placeholders.
Your code should look something similar to
$query = $this->Users->find()
->where([
'first_name LIKE' => '%:name%'
])
->bind(':name', $this->request->query('first_name'));
More information in:
Binding Values in Cookbook 3.x: Database Access & ORM
Query::bind()
You should consider using Search Plugin
Its just very simple, write this in controller
public $components = array(
'Search.Prg'
);
public function index() {
$this->Prg->commonProcess();
$this->set('users', $this->paginate($this->Users->find('searchable',
$this->Prg->parsedParams())));
}
And this one in Model
public $filterArgs = array(
'first_name' => array(
'type' => 'like',
'field' => 'first_name'
)
);
public function initialize(array $config = []) {
$this->addBehavior('Search.Searchable');
}
and you are done.
For more examples, visit here

CakePHP Tags Plugin Pagination Issue

I am having issues getting CakeDC's tag's plugin to work. I have read the documentation very carefully, but it seems the docs are very old.
// Totally works. Does what it is supposed to do, does not
// complain of missing models.
$tag = $this->Upload->Tagged->find('tagged',
array('by' => $tagname, 'model' => 'Upload', 'conditions' =>
array( 'Upload.soft_delete !=' => 1) ));
// 100% correct according to the 3 year old documentation.
// Complains of a missing "taggeds" model.
// Table taggeds for model Tagged was not found in datasource default.
// Undefined index: tagged [CORE/Cake/Model/Model.php, line 2731]
$this->paginate['Tagged'] = array(
'model' => 'Upload',
'tagged',
'by' => $tagname);
$tag = $this->paginate('Tagged');
I read the documentation here at: https://github.com/CakeDC/tags/wiki/Find-tagged-objects
At first, I experienced the Indirect modification of overloaded property $paginate ... no effect" bug until I added public $paginate = array(); to that the top of my controller. This has not helped the other error.
Hopefully I am missing something simple here.
UPDATE: I changed the code to look like this
$this->Paginator->settings['Tagged'] = array(
'tagged',
'model' => 'Upload',
'by' => $tagname
);
$this->Paginator->paginate('Tagged');
and I get this error: Error: Call to a member function paginate() on a non-object
I eventually made it work adding in the top of Controller
public $components = array('Paginator');
Then in my method
$this->Paginator->settings['Tagged'] = array(
'tagged',
'model' => 'Upload',
'by' => $tagname
);
$this->Paginator->paginate('Tagged');
I experienced the Indirect modification of overloaded property
$paginate ... no effect" bug
That's your issue and not really a bug. CakePHP has changed a little, try this:
$this->Paginator->settings['Tagged'] = array(
'tagged',
'model' => 'Upload',
'by' => $tagname
);
$this->Paginator->paginate('Tagged');
You're welcome to improve the documentation. We're maintaining currently 14 plugins for free, any kind of help is appreciated. Give something back and help improving the docs. :)

cakePHP virtualField within condition of another model

I have 2 models, User and Entity. I need to on my entities page, have pagination, and a simple search function. The search function will filter the entities, but the data it filters is a virtualField within the User model.
The Entity model:
public $belongsTo = array(
'User' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
)
};
The virtual field in the User model:
public $virtualFields = array("searchFor" => "CONCAT(User.first_name, ' ',User.last_name, ' ',User.email)");
And the condition within the entity controller:
$conditions["User.searchFor LIKE"] = "%".str_replace(" ","%",$this->passedArgs["searchFor"])."%";
$this->paginate = array(
'conditions' => $conditions,
'order' => array('Re.created' => 'DESC'),
'limit' => 20
);
From what I can read this is not possible because I cannot use virtualFields within a query of an associative model, and it says I must use it in "run time", but I really do not understand how to do that, or how to do that in this instance. Any ideas or a way I can get this to work?
You could try attaching the virtualField to the Entity model like so
$this->Entity->virtualFields['searchFor'] = $this->Entity->User->virtualFields['searchFor'];
But you have to make sure that a join is done and not 2 queries.
I believe its discussed in the book.
Edit: Book page
For this purpose I needed to do a search on a concatinated string from an associated model. Normally this can be done using Virtualfields in cake, but cake does not support using a virtualField from an associated model in the search.
Seeing that the 2 models were already linked with the belongsTo, I merely changed the condition to:
"conditions" => "CONCAT(User.first_name, ' ',User.last_name, ' ',User.email) LIKE" => "%".str_replace(" ","%",$this->passedArgs["searchFor"])."%"
Probably not the most elegant solution, but it works.

Using HtmlHelper on Model to insert links in returned errors

I'm working with CakePHP and trying to understand the best ways to make my application consistent and logical.
Now I'm trying to working with Model data validation and handling validation errors in the view, I have a doubt on how should I do if I like to insert some link inside the returned error, for example for a forgotten password.
Is it good to use (if it's possibile) HtmlHelper inside the Model to return consistent links inside my application, or should I think about another way?
<?php
App::import('Helper', 'Html');
class User extends AppModel {
var $name = 'User';
var $validate = array (
'email' => array (
'checkEmail' => array (
'rule' => array('email', true),
'message' => 'Email not valid message.'
),
'checkUnique' => array (
'rule' => 'isUnique',
'message' => 'This email is allready in the db, if you forgot the password, '.(string)$this->Html->link('click here', array('controller' => 'users', 'action' => 'password-recover')).'.'
)
)
// the rest of the code...
This doesn't work because it seems I can't chain the message string with HTML string.
Does exist e smartest way to do that, or should I simply insert the html string without the HtmlHelper?
If you really want HTML in your validation messages CakePHP provides a way to do this, no breaking Cake, no writing a lot of code.
In your $validation just use whatever HTML you would like to have presented to the user.
In your view when you create your FormHelper::input($fieldName, array $options) pass the following array to $options:
$options = array('error' => array(
'attributes' => array('escape' => false)
))
See this page to learn more about the $options['error'] ...options.
Alternatively, if you want all inputs with no HTML escaping you can pass $options['inputDefaults'] when you create the form.
this is a difficult topic because
you might need to break MVC
validation is as in your case usually in $validate and cannot contain dynamic stuff
for 1)
you can also use Router::url() with manual HTML
you can use BBcode or pseudo-markup and translate this into real links in the view/element of the flashmessage
for 2)
use __construct() and $this->validate to use dynamic elements if needed
In PHP, properties of a class (such as $validate) have to be initialized with constant values.
<?php
class User extends AppModel {
public $validate = array(
'email' => array(
'checkUnique' => array(
'rule' => array('isUnique'),
'message' => 'This email address has already been claimed, possibly by you. If this is your email address, use the reset password facility to regain access to your account'
),
),
);
public function beforeValidate($options = array()) {
$this->validate['email']['checkUnique']['message'] = String::insert(
$this->validate['email']['checkUnique']['message'],
array('link' => Router::url(array('action' => 'password-recover')))
);
return true;
}
You are making it hard on yourself. The helpers are not accessible in the model and controller. And for good reason: the M and C shouldn't be concerned with the V.
There are ways to do exactly as you want (but involves considerably more code). Since you ask for the smartest way: What's wrong with just echo the reset password link in the view, after the login form? Just echo 'Forgot your password? '.$this->Html->link('Click here', array('controller' => 'users', 'action' => 'password-recover'));
I don't agree on breaking the MVC logic. I also tried all the array('escape' => false) possible ways (in Form->input, in Form->error and even in the model) and none of them worked with me! (cakephp 2.0)
"Anh Pham" answer is the easiest and simplest way. In addition to that, I returned empty error message from model validation ('errorMessage' => false ; doesn't work in cakePhp 2.0).
Because I wanted to pass a variable to the view to build the link there (MVC), in the controller I check if the field is invalidated:
$invlaidFields = array_keys($this->Model->validationErrors();
if ( in_array('myField', $invalidFields) ){
...
}
In the view, I check if the field was invalidated, I then echo my error message giving it class error-message so it looks the same as the rest error messages.
if ($this->Form->('myFields')) { ... echo '<span class="error-message">error message'. $this->Html->link(...).'</span>'; }
Hope it helps somebody out there.
P.S. It's always a good practice to mention what cakePHP version you are using...
To cakephp2 you can use the following:
//model validation
'company' => array('notempty' => array('rule' => array('notempty'),'message' => "select one company o send email to contact",),)
//front
<?php if ($this->Form->isFieldError('Register.company')): ?>
<span class="text-danger"><?php echo $this->Form->error('Register.company', null, array('escape'=>false)); ?></span>
<?php endif; ?>

CakePHP bindModel HABTM Save

I want to create a binding from one model to my User model on the fly so that the JOIN is not called every time that I perform a find on that model. I am using the binding to perform a HABTM save. However, when I use the bindModel function, the HABTM data is not saved in the database.
What makes this odd is that if I move my binding to the User model, then the save works perfectly. I don't see any indication in the documentation that the save behavior would be different when the association is made in the model versus the bindModel function (though, I may have missed it if there is any).
Here is my bindModel code in my controller:
$this->User->bindModel(
array(
'hasAndBelongsToMany' => array(
'Othermodel' => array(
'className' => 'Othermodel',
'joinTable' => 'othermodels_users',
'foreignKey' => 'user_id',
'associationForeignKey' => 'othermodel_id',
'unique' => true,
)
)
)
);
if($res = $this->User->save($data)){
return true;
}
And this is my user model.
class User extends AppModel {
public $name = 'User';
public $belongsTo = array();
public $hasOne = array();
public $hasMany = array();
public $hasAndBelongsToMany = array(
'Othermodel' => array(
'className' => 'Othermodel',
'joinTable' => 'othermodels_users',
'foreignKey' => 'user_id',
'associationForeignKey' => 'othermodel_id',
'unique' => true
)
);
Again, I only have the relationship active in one place at one time, so I know the problem is not with the binding itself. It seems the problem is solely related to the fact that I have tried to use bindModel. Is this the intended behavior?
Based on the responses in this article, it looks like the bindModel function only exists for finds.
CakePHP: Bind Model not working
Though there's no information in the CakePHP 1.3 documentation that states that the bind is only for finds, though that would explain the behavior...
http://book.cakephp.org/1.3/en/The-Manual/Developing-with-CakePHP/Models.html
So, I am marking this as answered.
I'm not sure if this is relevant, but are you sure you are not doing any other find operations between the binding and the save. bindModel only binds the model for the next find operation so you may need to pass true through to bindModel to tell it to keep the binding around.
Relevant links: CakePHP: Bind Model not working and http://groups.google.com/group/cake-php/browse_thread/thread/316c9796603eac57?pli=1.
I hope this helps.
Try this,
$this->Message->bindModel(
array(
'belongsTo'=>array(
'User'=>array(
'foreignKey'=>false,
'conditions'=>array('Npl.to=User.id '),
'fields'=>array('recive')
),
'User_e'=>array(
'className'=>'User',
'foreignKey'=>false,
'alias'=>'User_e',
'conditions'=>array('Npl.from=User_e.id '),
'fields'=>array('recive')
)
)
)
);

Resources