How to fetch associated belongsToMany entities with CakePHP3 - php

I have Users and Courses table with belongsToMany relation. UserTable has
$this->belongsToMany('Courses', [
'foreignKey' => 'user_id',
'targetForeignKey' => 'course_id',
'joinTable' => 'courses_users'
]);
and CoursesTable has
$this->belongsToMany('Users', [
'foreignKey' => 'course_id',
'targetForeignKey' => 'user_id',
'joinTable' => 'courses_users'
]);
Now, I want to fetch courses with user_id. In my CoursesController, I tried
public function myCourses()
{
$id = $this->Auth->user('id');
$courses = $this->Courses->find('all',
['contain' => ['Users'],
'condition' => ['Courses.user_id' => $id]
]);
$this->set('courses', $courses);
}
when I debug($courses) with this code, I got '(help)' => 'This is a Query object, to get the results execute or iterate it.' message. I'm searching information and trying to do it for many hours but I can't make it. How can I fetch Courses datas with user_id? Thanks in advance.

If it's a has-and-belongs-to-many (HABTM) association with a join table of courses_users, you shouldn't even have a user_id field in your Courses table.
So now that we've determined you can't do what you were trying (Courses.user_id), we can look at what you thought you were trying:
$courses = $this->Courses->find('all',
['contain' => ['Users'],
//'condition' => ['Courses.user_id' => $id]
]);
This says "find all courses and any users that are associated with those courses".
But what you really WANT (I believe) is: "find all courses that belong to this specific user".
To do that, you'll want to use an matching() instead.
According to the CakePHP book:
A fairly common query case with associations is finding records
‘matching’ specific associated data. For example if you have ‘Articles
belongsToMany Tags’ you will probably want to find Articles that have
the CakePHP tag. This is extremely simple to do with the ORM in
CakePHP:
$query = $articles->find();
$query->matching('Tags', function ($q) {
return $q->where(['Tags.name' => 'CakePHP']);
});
So in your case, it would be something like this:
$query = $courses->find();
$query->matching('Users', function ($q) use ($id) {
return $q->where(['Users.id' => $id]);
});

Related

how to use laravel eloquent to get and display data from other tables

I was using these codes in my controller to get all the data from my 2 tables and it works fine
$All = Customers::with('order')->paginate(10);
return response()->json([
'code' => 0,
'success' => true,
'data' => $All
], 200);
Here is how I define the relationship between these 2 tables
class Customers extends Model
{
public function order()
{
return $this->hasMany(Orders::class, 'customer_id', 'id');
}
}
class Orders extends Model
{
public function customers()
{
return $this->belongsTo(Customers::class, 'customer_id', 'id');
}
}
Now my desire output is to hide the order id, order timestamps and change the customer_id to customer's name (the customer's name is not in my orders db table).
I'm using 'data' => DataResource::collection($All) in my controller and this is my DataResource
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'order' => $this->order
];
}
and of course the output is same with the image above.
My database structure:
orders table:
customer table:
Can anyone help me with that?
The answer is simple and basically a copy of the official documentation. You simply need to wrap your orders in an OrderResource as well.
// DataResource
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
'order' => OrderResource::collection($this->order)
];
}
// OrderResource
public function toArray($request)
{
return [
'items' => $this->items,
'quantity' => $this->quantity
];
}
I don't really understand why you would want to include the customer_name in your orders when it is already present on the customers object one hierarchy above. But if you really want to add it, you should be able to do so with: 'customer_name' => $this->customers->name.
As a side note: you really should be more consistent with your naming. Why is the resource called DataResource when it is about Customers? Why is your model called Customers in plural form rather than Customer in singular, which is the convention (and more logical if you consider that one model represents one customer). Why is your belongsTo relation called customers() in plural when it returns one customer, while your hasMany relation is called order whereas it returns one or more orders?

CakePHP associations relations table

I need to build the associations like Group hasMany users and User belongToMany groups.
But I can't get the right result, it always use the wrong table instead groups_relations
My models:
class GroupsTable extends Table
{
public function initialize(array $config)
{
$this->setTable('groups');
$this->setDisplayField('title');
$this->setPrimaryKey('id');
$this->hasMany('Users', [
'joinTable' => 'groups_relations',
'foreignKey' => 'user_id',
]);
}
}
class UsersTable extends Table
{
public function initialize(array $config)
{
$this->table('user_users');
$this->belongsToMany('Groups', [
'joinTable' => 'groups_relations',
'foreignKey' => 'group_id',
]);
}
}
class GroupsRelationsTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);
$this->setTable('groups_relations');
$this->setDisplayField('group_id');
$this->setPrimaryKey('id');
$this->belongsTo('Groups', [
'foreignKey' => 'group_id',
'joinType' => 'INNER'
]);
$this->belongsToMany('Users', [
'foreignKey' => 'user_id',
'joinType' => 'INNER'
]);
}
}
And my table groups_relations:
id | group_id | user_id
I run query as:
$groupsWithUsers = $this->Groups->find('all', array(
'contain' => array('Users')
));
I can't understand how to tell to cake use my intermediary table and append reuslts to array.
joinTable is not a valid configuration key for a hasMany association. I think that you want to have Groups belongsToMany Users. Another clue about this is that hasMany is the "opposite" of belongsTo, while belongsToMany is it's own opposite. (That is, if A hasMany B, then B belongsTo A, but if A belongsToMany B, then B belongsToMany A.) Note that you will also want to change your GroupsRelations association with Users to belongsTo.
Is this code that was baked for you? Because it should know better. When I run into sticky association problems, I sometimes have Cake bake the model code for me, and then look at how the result differs from what I've written.
Rather than trying to use the relation the way you are doing why not just select from the relations table in the first place. This seems like the more Cake way of doing things. You can exclude the conditions clause if you want all data back.
$groupsWithUsers = $this->GroupsRelations->find('all', array(
'contain' => ['Users', 'Groups'],
'conditions' => ['Group.id' => $id]
)
);
After further looking into this I found something I have not used but seems to fit exactly what you need its a belongsToMany using an intermediary table. In your table file for the users you would add the following. A similar entry would be added to the group page.
$this->belongsToMany('Groups', [
'through' => 'GroupRelations',
]);

Saving HasMany Associations Data in CakePHP 3.x

I am having two tables. My primary table is Students. And my secondary table is Exams. I am trying to save both the tables using hasMany and belongsToMany Association. But It is saving data in Student table only, not in Exams. Can any one help me to resolve this problem.
Students Model :
class StudentsTable extends Table {
public function initialize(array $config) {
$this->addBehavior('Timestamp');
parent::initialize($config);
$this->table('students');
$this->primaryKey(['id']);
$this->hasMany('Exams', [
'className' => 'Exams',
'foreignKey' => 'student_id',
'dependent'=>'true',
'cascadeCallbacks'=>'true']);
}
}
Exams Model :
class ExamsTable extends Table {
public function initialize(array $config) {
parent::initialize($config);
$this->table('exams');
$this->primaryKey(['id']);
$this->belongsToMany('Students',[
'className'=>'Students',
'foreignKey' => 'subject_id',
'dependent'=>'true',
'cascadeCallbacks'=>'true']);
}
}
My school.ctp :
echo $this->Form->create();
echo $this->Form->input('name');
echo $this->Form->input('exams.subject', array(
'required'=>false,
'multiple' => 'checkbox',
'options' => array(
0 => 'Tamil',
1 => 'English',
2 => 'Maths')));
echo $this->Form->button(__('Save'));
echo $this->Form->end();
In my controller:
public function school() {
$this->loadModel('Students');
$this->loadModel('Exams');
$student = $this->Students->newEntity();
if ($this->request->is('post')) {
$this->request->data['exams']['subject'] =
implode(',',$this->request->data['exams']['subject']);
$student = $this->Students->patchEntity(
$student, $this->request->data, ['associated' => ['Exams']]
);
if ($this->Students->save($student)) {
$this->Flash->success(__('The user has been saved.'));
} else {
$this->Flash->error(__('Unable to add the user.'));
}
}
}
Patching BelongsToMany Associations
You need to make sure you are able to set exams. Set accessibleFields to allow you to patch associated data
$student = $this->Students->patchEntity(
$student, $this->request->data, [
'associated' => ['Exams'],
'accessibleFields' => ['exams' => true]
]
);
You can also do this with the $_accessible property in the entity.
I've never done hasMany to belongsToMany because i don't think it works that way (I mean no harm in my words.) But I'll try to explain. Your relationships should be both belongsToMany because exams will have many students and students will have many exams. So basically they're the same either way. What you need is another table to connect them which will be called students_exams or exams_students (i think its exams_students because E comes before S) because in cake if you name everything properly most of it happens automatically.
Assuming you know how patchEntity works, creating your $this->request->data properly will patch it automatically and save it in the correct table when you save it. If you have any more questions feel free to ask more. :)

Eloquent automatically selects unwanted columns upon eager-loading model relationship

I am converting an internal API from HTML (back-end) processing to JSON (using Knockout.js) processing on the client-side to load a bunch of entities (vehicles, in my case).
The thing is our database stores sensitive information that cannot be revelead in the API since someone could simply reverse engineer the request and gather them.
Therefore I am trying to select specifically for every relationship eager-load the columns I wish to publish in the API, however I am having issues at loading a model relationship because it seems like Eloquent automatically loads every column of the parent model whenever a relationship model is eager loaded.
Sounds like a mindfuck, I am aware, so I'll try to be more comprehensive.
Our database stores many Contract, and each of them has assigned a Vehicle.
A Contract has assigned an User.
A Vehicle has assigned many Photo.
So here's the current code structure:
class Contract
{
public function user()
{
return $this->belongsTo('User');
}
public function vehicle()
{
return $this->belongsTo('Vehicle');
}
}
class Vehicle
{
public function photos()
{
return $this->hasMany('Photo', 'vehicle_id');
}
}
class Photo
{
[...]
}
Since I need to eager load every single relationship listed above and for each relationship a specific amount of columns, I need to do the following:
[...]
$query = Contract::join('vehicles as vehicle', 'vehicle.id', '=', 'contract.vehicle_id')->select([
'contract.id',
'contract.price_current',
'contract.vehicle_id',
'contract.user_id',
'contract.office_id'
]);
[...]
$query = $query->with(['vehicle' => function ($query) {
$query->select([
'id',
'trademark',
'model',
'registration',
'fuel',
'kilometers',
'horsepower',
'cc',
'owners_amount',
'date_last_revision',
'date_bollo_expiration',
'bollo_price',
'kilometers_last_tagliando'
]);
}]);
$query = $query->with(['vehicle.photos' => function ($query) {
$query->select([
'id',
'vehicle_id',
'order',
'paths'
])->where('order', '<=', 0);
}]);
$query = $query->with(['user' => function ($query) {
$query->select([
'id',
'firstname',
'lastname',
'phone'
]);
}]);
$query = $query->with(['office' => function ($query) {
$query->select([
'id',
'name'
]);
}]);
[...]
return $this->response->json([
'error' => false,
'vehicles' => $vehicles->getItems(),
'pagination' => [
'currentPage' => (integer) $vehicles->getCurrentPage(),
'lastPage' => (integer) $vehicles->getLastPage(),
'perPage' => (integer) $vehicles->getPerPage(),
'total' => (integer) $vehicles->getTotal(),
'from' => (integer) $vehicles->getFrom(),
'to' => (integer) $vehicles->getTo(),
'count' => (integer) $vehicles->count()
],
'banner' => rand(0, 2),
'filters' => (count($input) > 4),
'filtersHelpText' => generateSearchString($input)
]);
The issue is: if I do not eager load vehicle.photos relationship, columns are loaded properly. Otherwise, every single column of Vehicle's model is loaded.
Here's some pictures so you can understand:
Note: some information have been removed from the pictures since they are sensitive information.
You can set a hidden property on your models which is an array of column names you want to hide from being output.
protected $hidden = ['password'];

'Through' association with 'foreign_key' & 'model' fields in CakePHP 3

This is a pretty standard thing that I've done probably 600 times in CakePHP 2, but for the life of me, I can't get it to work in CakePHP 3.
I have Videos, Photos, Articles. I also have Categories.
Videos, Photos, and Articles can all belong to one or more Categories.
The goal of the current problem is to pull videos that are under a certain category.
So, I tried this:
// VideosTable
$this->belongsToMany('Categories', [
'joinTable' => 'categorized',
'className' => 'Categorized',
'foreignKey' => 'foreign_key',
'conditions' => [
'Categorized.model' => 'Videos',
]
]);
public function getTopVideosByCategory($categorySlug)
{
return $this->Categories->find('all')
->where(['Categories.slug' => $categorySlug])
->contain([
'Videos' => function ($q) {
return $q
->limit(8)
->contain([
'Tags',
'Categories' // tried with and without this
])
->order([
'Videos.featured' => 'DESC',
'Videos.created' => 'DESC'
]);
}
])
->first();
}
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column
'Categorized.model' in 'where clause'
I've tried a number of other ways including creating the join table's model, and a few others, but keep getting errors. I've tried with every option, and with limited number of options. I've tried using an actual Table class, and I've tried a pseudo one (like "Categorized" above).
I have to assume this is pretty standard, but can't find an example in the book, and I just can't seem to get it to work.
Edit:
I've also tried this:
//VideosTable
public function initialize(array $config)
{
$this->belongsToMany('Categories', [
'through' => 'Categorized',
'conditions' => [
'Categorized.model' => $this->alias(),
]
]);
}
public function getTopVideosByCategory($categorySlug)
{
return $this->find('all')
->matching('Categories', function ($q) use ($categorySlug) {
return $q
->where(['Categories.slug' => $categorySlug]);
})
->contain([
'Tags',
'Categories'
])
->limit(8)
->order([
'Videos.featured' => 'DESC',
'Videos.created' => 'DESC'
])
->first();
}
But get this error:
Error: SQLSTATE[42S22]: Column not found: 1054 Unknown column
'Categorized.model' in 'on clause'
Since Videos and Categories is not a 1-1 o n-1 (hasOne or belongsTo), it is impossible to build a SQL expression that can include conditions for the other table. For those cases, CakePHP implements the matching() function. It works similar to contain() but what it does is using an INNER join to get the data from the external associations:
http://book.cakephp.org/3.0/en/orm/retrieving-data-and-resultsets.html#filtering-by-associated-data
You can also look an an example of using it here:
http://book.cakephp.org/3.0/en/tutorials-and-examples/bookmarks/intro.html#creating-the-finder-method
I ended up getting it to work like this:
class VideosTable extends Table
{
public function initialize(array $config)
{
$this->hasMany('Categorized', [
'foreignKey' => 'foreign_key',
'conditions' => [
'Categorized.model' => $this->alias(),
]
]);
}
public function getTopVideosByCategory($categorySlug)
{
return $this->find()
->matching(
'Categorized.Categories', function ($q) use ($categorySlug) {
return $q
->where(['Categories.slug' => $categorySlug]);
})
->limit(8)
->order([
'Videos.featured' => 'DESC',
'Videos.created' => 'DESC'
])
->all();
}
I've up-voted José's answer, as it led me down the road of figuring it out, but will mark this as the answer, as I think it more-quickly helps users trying to figure this particular problem out.
José, if you want to append this (with any tweaks you see fit) to your answer, I'll change the marked answer to yours.
It seem that what you want is:
$this->belongsToMany('Categories', [
'through' => 'Categorized',
'conditions' => ['Categorized.model' => $this->alias()]
]);

Categories