Cakephp many to many with same table - cakephp

I am new to CakePHP so please be easy on me.
I have this issue that I don't seem to be able to solve. I have a "contact" table and would like this table to have a many to many relationship with itself. The objective is to show that a contact is related to other contacts from the same company. So when clicking a contact's profile, I will be able to view other contacts that are related to that particular person.
I have no idea how to approach this issue. I'm thinking of a join table but doesn't seem to be possible? or I might be wrong. If anyone has any idea how to do this, please leave as much detail as possible so that I can understand how to do this (as I'm new to CakePHP as stated earlier).
Thanks!!
Edit: Tried the method stated by User996302 but still not working.
This is a portion of the Contact model for the relationship:
public $hasAndBelongsToMany = array( 'Relation' => array( 'className' => 'Contact', 'joinTable' => 'contacts_contacts', 'foreignKey' => 'contact_id', 'associationForeignKey' => 'related_id', 'unique' => true, 'conditions' => '', 'fields' => '', 'order' => '', 'limit' => '', 'offset' => '', 'finderQuery' => '', 'deleteQuery' => '', 'insertQuery' => '' ) );
I am using the saveAll function in the controller.
In the add section of the controller, I have this at the end:
$relations = $this->Contact->Relation->find('list'); $this->set(compact('relations'));
In the add view page I have this line of code to display the selection box:
echo $this->Form->input('Relation', array('type'=>'select', 'multiple' => true));
I have a contacts and a contacts_contacts table. The contacts_contacts table has three attributes: id, contact_id, related_id.
I have done all these but the selection list in the form does not display any contact that I can choose from.
Not sure where the problem is. Any help will be great!

Those links should help you: example, SO question on the question.
Edited to correct a mistake.

Related

Get original associations after using Containable behavior in CakePHP

Background: CakePHP 2.6.3. A pretty stable app. New behavior (MyCustomBehavior) created to output some extra info.
I have a Model MyModel acting as Containable (defined in AppModel) and then MyCustom (defined in MyModel). MyCustomBehavior is written in a way that it needs to work with the Model's associations with other Models in my app.
Problem: Whenever I contain related models in my find() call of MyModel, I cannot get a complete list of MyModel associations because Containable behavior unbinds the models that are not contained. However, if I don't set contain in my find() options or set 'contain' => false everything works as expected.
Sample MyModel->belongsTo
public $belongsTo = array(
'MyAnotherModel' => array(
'className' => 'MyAnotherModel',
'foreignKey' => 'my_another_model_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Creator' => array(
'className' => 'User',
'foreignKey' => 'user_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Approver' => array(
'className' => 'User',
'foreignKey' => 'approver_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
'Status' => array(
'className' => 'Status',
'foreignKey' => 'status_id',
'conditions' => '',
'fields' => '',
'order' => ''
),
);
Sample find()
$this->MyModel->find('all', array(
'fields' => array(...),
'conditions' => array(...),
'contain' => array('Approver', 'Status')
));
Result of MyModel->belongsTo in MyCustomBehavior::beforeFind()
$belongsTo = array(
'Approver' => array(
...
),
'Status' => array(
...
),
);
Expected MyModel->belongsTo in MyCustomBehavior::beforeFind()
$belongsTo = array(
'MyAnotherModel' => array(
...
),
'Creator' => array(
...
),
'Approver' => array(
...
),
'Status' => array(
...
),
);
Obvious solution: One dumb way to solve the problem is to simply set Containable behavior in MyModel instead of AppModel to control the order of loading the behaviors, i.e., public $actsAs = ['MyCustom', 'Containable']. This solution is not the best because there may be other behaviors in other models that depend on Containable, so the order of Containable needs to set in each model in app explicitly and hope that I didn't break the app somewhere.
A similar(related) question was asked on SO here but has no answers.
Need a more robust solution that can address the needs of MyCustomBehavior without having to make changes in rest of the app and looking out for any unexpected behavior.
Attempt 1 (Imperfect, error prone):
One way to recover all the original associations is to call
$MyModel->resetBindings($MyModel->alias);
$belongsToAssoc = $MyModel->belongsTo; // will now have original belongsTo assoc
However, this approach it may fail (SQL error 1066 Not unique table/alias) to work correctly if I had used joins in my find call (using default alias) to explicitly join to an already associated model. This is because Containable will also attempt to join all these tables restored by resetBindings() call resulting in join being performed twice with same alias.
Attempt 2 (perfect#, no known side effects##):
Further digging through the core Containable behavior and docs led me to object $MyModel->__backOriginalAssociation and $MyModel->__backAssociation (weird enough that ContainableBehavior never used $__backContainableAssociation as the variable name suggests) that was created and used by this behavior to perform resetBindings(). So, my final solution was to simply check if Containable is enabled on my modal (redundant in my case because it is attached in AppModel and is never disabled or detached throughout the app) and check if the object is set on the model.
// somewhere in MyCustomBehavior
private function __getOriginalAssociations(Model $Model, $type = 'belongsTo') {
if(isset($Model->__backAssociation['belongsTo']) && !empty($Model->__backAssociation['belongsTo'])) { // do an additional test for $Model->Behaviors->enabled('Containable') if you need
return $Model->__backAssociation[$type];
}
return $Model->$type;
}
public function beforeFind(Model $Model, $query) {
// somewhere in MyCustomBehavior::beforeFind()
...
$belongsToAssoc = $this->__getOriginalAssociations($MyModel, 'belongsTo'); // will now have original belongsTo assoc
...
return $query;
}
$__backAssociation holds model associations temporarily to allow for dynamic (un)binding. This solution can definitely be further improved by merging results of $Model->belongsTo and $Model->__backAssociation['belongsTo'] (or hasMany, hasOne, hasAndBelongsToMany) to include any models that were bound on the fly. I don't need it, so I will skip the code for merging.
# Perfect for my own use case and my app setup.
## No side effects were found in my testing that is limited by my level of expertise/skill.
Disclaimer: My work in this post is licensed under WTF Public License(WTFPL). So, do what the f**k you want with the material. Additionally, I claim no responsibility for any financial, physical or mental loss of any kind whatsoever due to the use of above material. Use at your own risk and do your own f**king research before attempting a copy/paste. Don't forget to take a look at cc by-sa 3.0 because SO says "user contributions licensed under cc by-sa 3.0 with attribution required." (check the footer on this page. I know you never noticed it before today! :p)

CakePHP should I be using keepExisting in a HABTM relationship?

I'm new to Cake, and trying to figure out how this is possible or if I'm going about it the wrong way.
Question hasMany Answer
Quiz HABTM Answer
So I have the following tables: quizzes, answers, quizzes_answers
However, there's one type of question that doesn't fit the mold. This type of question needs to record an image_id. So I was thinking of storing this data in the association table, but I'm not sure how to use the HABTM setting:
'unique' => 'keepExisting'
It doesn't seem like this will work to save the data I need. And if so, how would I structure the form in the view to save this data?
Or... should I be creating another Model? If I cannot successfully save an image_id in the associated table, maybe the alternative is to have another association with the Quiz model and a new model?
Quiz hasMany StudentAnswer
Where I'd store the quiz_id, question_id, answer_id, and image_id. The last one: image_id would be 0 unless the question being asked is of a specific type. In this situation I would remove the HABTM association above.
Hopefully this is clear. I can elaborate further if necessary.
Update:
I'm able to save quiz_id, answer_id, and image_id for the quizzes_answers table. However, if more updates are made the prior associations are deleted. And I have unique set to keepExisting in the models.
public $hasAndBelongsToMany = array( // Quiz Model
'Answer' => array(
'className' => 'Answer',
'joinTable' => 'quizzes_answers',
'foreignKey' => 'quiz_id',
'associationForeignKey' => 'answer_id',
'unique' => 'keepExisting',
'dependent' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
public $hasAndBelongsToMany = array( // Answer Model
'Quiz' => array(
'className' => 'Quiz',
'joinTable' => 'quizzes_answers',
'foreignKey' => 'answer_id',
'associationForeignKey' => 'quiz_id',
'unique' => 'keepExisting',
'dependent' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'finderQuery' => '',
'deleteQuery' => '',
'insertQuery' => ''
)
There is no HABTM in the question
In the question it's described that there is:
Quiz hasAndBelongsToMany Answer
i.e. there is a table like this:
answers_quizes
answer_id
quiz_id
What about who answered the question? Unless there is only one student ever taking a given quiz - a habtm association is inappropriate.
Create a model for the join table
especially in light of previous point
Here's a simple rule, which makes some schema/code decisions easy:
A join table with more than 2 fields is not a join table. It is a model in its own right and needs a primary key.
That doesn't mean you can't use habtm - it just means the choice is there for when to do so (reading data perhaps) and when not (saving).
keepExisting?
Keep existing means:
When [unique is set to] keepExisting, the behavior is similar to true, but existing associations are not deleted.
That is, if there is existing habtm data for a given user and question, it will not be deleted before inserting the new records. That would be appropriate if for example the one user was allowed to answer the same question multiple times and you want to store all attempts, instead of just the last one.

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.

Best way to load models in CakePHP 2.0

I'm not sure of the best way to load models in CakePHP 2.0 now.
Question 1
I have a model where more than one database field is related to another model.
customers table has the fields country_origin, country_residence and country_study and all of those fields contain an ID from the table countries.
So in my Customer model, how am I supposed to load the Country model?
Question 2
Has Controller::loadModel() been deprecated or is it bad practice to use this? How am I supposed to load a model in the controller?
Question 3
When or why do you have to use App::uses() in a controller or model? I don't understand the point when the models will load anyway if you use the normal methods like loadModel(), hasOne, hasMany, belongsTo, etc.
This should be simple to understand. If you are using a controller and you need to load another model, you can call:
$this->loadModel('SomeModel');
Then you can make calls to the model like you normally would:
$this->SomeModel->read(null, '1');
App::uses is for lazy loading of classes. So you can still load a model using uses:
App::uses('MyModel', 'Model');
But then you will need to call it differently:
$MyModel = new MyModel();
$MyModel->read(null, '1');
or
MyModel::read(null, '1');
It just depends on where and how you want to use it.
The preferred way is
$this->load('MyModel');
However, you can also use
public $uses = array('DefaultModel', 'MyModel');
.
.
$this->MyModel->field(...);
Cake supports both and you are free to use anyone you like.
For Question 1
As per your structure there is an association between Customer and Country model i think so we don't need to load model.
We can create virtual association for each id such as,
'CountryOrigin' => array(
'className' => 'Country',
'foreignKey' => 'country_origin_id',
'dependent' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '',
'counterQuery' => ''
)
'CountryResidence' => array(
'className' => 'Country',
'foreignKey' => 'country_residence_id',
'dependent' => true,
'conditions' => '',
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '',
'counterQuery' => ''
)
By doing this we can create an association between models so we don't want to load model explicitly
Loading model is good when we don't have an association for that we can use as,
Syntax for load Model is
For single model load
$this->loadModel('Country');
This will be more useful if we want to load model for particular action or condition
If we wan to use throughout the controller we can use $uses variable
For multiple model load.
public $uses = array('Country','OtherModel');
we can access model like,
$this->Country->find('count');

CakePHP, HasMany Relationship

Is it somehow possible to create a hasMany Relationship which makes use of an ID outside of the Model? For example one User has many Comments, but I would like to find just the comments of the logged in user:
public $hasMany = array(
'MyComment' => array(
'className' => 'Comment',
'foreignKey' => 'user_id',
'dependent' => false,
'conditions' => array('Comment.user_id' => $loggedinUser_id),
'fields' => '',
'order' => '',
'limit' => '',
'offset' => '',
'exclusive' => '',
'finderQuery' => '',
'counterQuery' => ''
)
);
I was thinking of passing the $loggedinUser_id in the controllers beforeFilter() to the model. Is this a good way to solve this issue or are there better ways?
Any suggestion is appreciated.
Thanks a lot!
I would advise following the "fat model, skinney controller" way of thinking. I would build a function in the model called: user_only_comments (or something that would make sense to you).
// in the comment model
function user_only_comments($id) {
return $this->find('all', array('conditions' => array('Comment.user_id' => $id)));
}
Then in the controller, you just call:
$user_comments = $this->Comment->user_only_comments($user_id);
Then you are good to go and your controller is not only nice and clean, but you can call this model specific method anywhere without having to write the conditions every time. If the conditions change, you can change it on one place in stead of everywhere in the controller you set the condition.
Are you always and only going to want to access the logged in user's comments? Aren't you going to want to show other people's comments at some point in time?
I'd suggest you just set that as a condition in each call to Model::find() from your controller.
Also, depending on specific types of relationships, you may need to use the Containable behavior to filter based on related model criteria (generally for HABTM rather than HM).

Resources