CakePHP 3.1, universal BelongsToMany - cakephp

I made myself a plugin for loading related content with beforeFind(), so you could say that ContentPage/10 is similar to ConentNews/10 and Gallery/5.
My table related_contents looks like:
id int(11)
source_table_name varchar(255)
target_table_name varchar(255)
source_table_id int(11)
target_table_id int(11)
My code in behavior:
public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary) {
$attachedTables = self::getAttachedTables(); //Array of ORM models which has this behavior
foreach ($attachedTables as $attachedTable) {
$options = [
'through' => 'RelatedContents',
'bindingKey' => 'RelatedContents.source_table_id',
'foreignKey' => 'RelatedContents.target_table_id',
'conditions' => [
'RelatedContents.source_table_name' => $this->_table->table(),
'target_table_name' => $attachedTable->table(),
]
];
$this->_table->belongsToMany($attachedTable->alias(), $options);
}
}
Now, when i try to find() in my model, zero related entities are found with no error. What am i doing wrong?

If calling your getAttachedTables() method on intialization time causes recursion, then there might be something that needs to be fixed. Reassociating on every find doesn't seem like an overly good idea, unless you really need to do so because the associations have changed/need to change, or have been removed.
That being said, your problem most probably is the missing targetForeignKey option, the bindingKey option that you are using is not available for BelongsToMany associations! Also note that the foreign keys need to be defined without aliases!
Also you're also missing an alias in the conditions, but that shouldn't have anything to do with the problem that no associations are being fetched.
So, foreignKey should be set to the FK column in the current model, and targetForeignKey to the FK column of the target model, like
$options = [
// ...
'foreignKey' => 'source_table_id',
'targetForeignKey' => 'target_table_id',
// ...
];
See also
Cookbook > Database Access & ORM > Associations > BelongsToMany Associations

Related

How to get property of associated table?

I was setup cake php for website: http://bomnuocebara.com but i have error.
I have very basic question but I'm kind of stuck with the syntax here:
I have an entity query. The entity is associated to another table which has a 'name' property? (many to many association)
My $query looks like this when I debug it:
/plugins/MailCalculator/src/Controller/PostalServicesController.php (line 140)
object(MailCalculator\Model\Entity\PostalService) {
'id' => (int) 1,
'carrier' => 'Deutsche Post'
},
'modified' => null,
'_matchingData' => [
'Insurances' => object(MailCalculator\Model\Entity\Insurance) {
'id' => (int) 2,
'name' => 'Wert',
'price' => (float) 4.3,
...
basically the question is, how do I get the values out from the __matchingData array?
I thought the syntax for it should be something like:
$var = $query->insurance->name or $var = $query->insurances['name'] but both things debugged give me 'null'
This should work with your current results.
$var = $query->_matchingData['Insurances']->name
Note, that if you contain association to your query, it should be available also as standard property in result set.
http://book.cakephp.org/3.0/en/orm/query-builder.html#filtering-by-associated-data
The data from the association that is ‘matched’ will be available on
the _matchingData property of entities. If you both match and contain
the same association, you can expect to get both the _matchingData and
standard association properties in your results.

Save method don't work when I have both x and x_id fields in my table in cakephp 3

I have both language and language_id fields in my users table. When I want to save my data, save method returns false.
//Table/UsersTable.php
$this->belongsTo('Languages', [
'alias' => 'Languages',
'foreignKey' => 'language_id'
]);
When I remove this code or remove language field from database, save method works properly.
Yeah, remember that CakePHP reserves the a property name where it will store the association data for each of your associations. In Your case, for the Languages association it will use the language property. If you already have a field with the same name, You can configure your association to use another property name:
$this->belongsTo('Languages', [
'alias' => 'Languages',
'foreignKey' => 'language_id',
'propertyName' => 'preferred_language'
]);
Personally, I would stick to conventions and not have a language field in the database, it make very little sense when you already have a language_id field.

CakePHP conditions clause on associations when using Model::find()

I just confused because of a find() result. This is my configurations:
First, users can have different User.role values: student, admin, and some others.
// Book
public $belongsTo = array(
'Student' => array(
'className' => 'User',
'foreignKey' => 'student_id'
'conditions' => array('User.role' => 'student')
);
);
When I chain Models like $this->Book->Student->find('list'); I was expecting to get only users whose role are 'student', but instead, it gets all users. What is going on here, what is conditions for on association definition, where can it and cannot be used. Any lead would help, thanks.
PS: I am aware that I could put conditions on find(), that's not the issue
There is a difference between associated data and accessing an associated model object. If you access $this->Book->Student you're accessing the Student model and work in it's scope. The conditions in the defined associations work only in the context of the accessed object.
So if you do a find on the Book and list the students for that book:
$this->Book->find('first', array('contain' => array('Student'));
Your code will work correctly. It will find the book plus the first user who has the role stundent. BUT your association is wrong then: It should be hasMany. because why would you filter a book by role if the book just belongsTo one student?
If you want to filter users by their role you can implement a query param that is checked in beforeFind(), pseudocode: if isset roleFilter then add contention to filter by that role from roleFilter.
Or, if you don't need to paginate just create a getStudents() method in the user model that will return a find('list') that has the conditions.
Or Student extends User and put the filter in the beforeFind() and use that model instead of the User model in your Book association.
If you want to filter on model level or per model I think the last one is a good option. Don't forget to set $useTable and $name or the inherited model will cause problems.
you have miss , inside your model.
try this:
public $belongsTo = array(
'Student' => array(
'className' => 'User',
'foreignKey' => 'student_id', //<------ miss
'conditions' => array('User.role' => 'student')
);
);
Yoi can debug your query to check what is the real query that you make.
Personally I have never use this approach, I prefer to use foreign key with another table for examples Rolesand User.role_id.
Is better for me to use this approach to have more flexibility inside your app.
After I prefer to use a conditions where inside controller to check well the query, because in your way every query you search always for student role not for the other and can be a problem for the rest of role, because inside controller you see a find without conditions but it doesn't take right value because in your model there is a particular conditions.
For me the good way is to create a new table, use foreign key and where conditions inside action of the controller to view well what are you doing.
For default all relations are "left join", you must set the parameter "type" with "inner" value
// Book
public $belongsTo = array(
'Student' => array(
'className' => 'User',
'foreignKey' => 'student_id'
'conditions' => array('Student.role' => 'student'), // <-- Fix That (field name)
'type' => 'inner', // <-- add that
);
);

CakePHP - Retrieving a list of options from another table, to use with form helper/ before Instering

Here is Yet another cakePHP question! I have table called blood_groups which has blood_group_id and group fields.. Then I have another table called donors, which has several fields such as name, surname etc. Another field included inside this table is the foreign key 'blood_group_id' which will need to map to the blood_group table on retrieval. in the donor registration view, i want to be able to retrieve the values from the blood_groups table, and display them using the formHelper (with their respective id's).
I have gone through CAKE doc, and I understand that I would need to create the association between my models, but I am struggling to figure this one out. Should I create $hasOne association inside the Donor Model (considering that the Donor table has the fk of the other table). And how would I go about retrieving the options of blood_groups from the blood_groups Model?
Should It work like this?(and are any other prerequisites involved?) :
In my DonorController -
$this->set('blood_groups', $this->Donor->Blood_Group->find('all'));
in Views/Donor/add.ctp
echo $this->Form->input('blood_group_id');
Accessing data through associations is fine. But for radios or checkboxes you want to do a find('list). Your model and variable name does not match the CakePHP convention, there should be no underscore.
Properly named this should be already enough to populate the input.
// controller
$this->set('bloodGroups', $this->Donor->BloodGroup->find('list'));
// view
echo $this->Form->input('blood_group_id');
If you don't follow the conventions for some reason:
echo $this->Form->input('blood_group_id', array(
'options' => $bloodGroups
));
See:
Linking Models Together
The Form Helper
Create one function in BloodGroup Model
function getDonors(){
$options = array(
// 'conditions' => array('Donor.blood_group_id'=>$id),
'joins' => array(
array(
'alias' => 'Donor',
'table' => 'donors',
'type' => 'LEFT',
'conditions' => array(
'Donor.blood_group_id = BloodGroup.blood_group_id',
),
)
),
'fields' => array('Donor.name','Donor.surname','Donor.blood_group_id',
'BloodGroup.blood_group_id')
);
$returnData = $this->find('all',$options);
return $returnData;
}
Now from controller call this function
App::import('model','BloodGroup');
$BloodGroup = new BloodGroup;
$donorList = $BloodGroup->getDonors();
$this->set('donorList',$donorList);
In view file you will get list of donors in $donorList.

Pagination with recursion while using loadModel

In my "Reports" controller, which is just a dummy controller without any actual database, I'm trying to generate a paginated view of other models. For example, to generate paginated view of "Transactions" model I'm doing the following:
$this->loadModel('Transactions');
$this->Transactions->bindModel(array('belongsTo'=>array('Item'=>array('className'=>'Item'),'Member'=>array('className'=>'Member'))));
$results = $this->paginate('Transactions',null,array('recursive'=>1));
But this is not giving me associated data from Items and Members. If I do a
$this->Transactions->find('all',array('recursive'=>1))
I get the associated data, but not paginated. How will I get paginated view which includes the associated data too?
Two things: First, even when plural model names can work for some odd reason, the convention is that model names are singular, like $this->loadModel('Transaction');. See the manual on naming conventions.
Second, forget about recursive and go for the Containable behavior. Frankly, it's so useful that I wonder why it isn't the default process (perhaps because Containable got created when the framework was very mature). Matt has a good book explaining why Containable is good (download it, really, it's almost mandatory :D ). But to help even more, I'm going to tell you exactly how you solve your issue with containable:
1) Define the associations in the models, like:
In Transaction model:
var $belongsTo = array(
'Item' => array(
'className' => 'Item',
'foreignKey' => 'item_id',
)
);
In Item model:
var $hasMany = array(
'Transaction' => array(
'className' => 'Transaction',
'foreignKey' => 'item_id',
'dependent' => true,
'exclusive' => true,
)
);
Do the same for the Member model.
2) Create an app_model.php file in /app/ with this code:
(The $actsAs variable here within the AppModel class tells all models to use Containable)
<?php
class AppModel extends Model {
var $recursive = -1;
var $actsAs = array('Containable');
}
?>
3) In the Reports Controller, change the code to something like this:
(The contain parameter is an array of all the associated models that you want to include. You can include only one assoc. model, or all, or whatever you want).
$this->loadModel('Transaction');
$this->paginate = array('Transaction' => array('contain' => array('Item', 'Member')));
$results = $this->paginate('Transaction');
And that's it!

Resources